import PromiseKit
// A f e w n o t e s a b o u t m a k i n g c h a n g e s i n t h i s f i l e :
//
// • D o n ' t u s e a d a t a b a s e t r a n s a c t i o n i f y o u c a n a v o i d i t .
// • I f y o u d o n e e d t o u s e a d a t a b a s e t r a n s a c t i o n , u s e a r e a d t r a n s a c t i o n i f p o s s i b l e .
// • C o n s i d e r m a k i n g i t t h e c a l l e r ' s r e s p o n s i b i l i t y t o m a n a g e t h e d a t a b a s e t r a n s a c t i o n ( t h i s h e l p s a v o i d n e s t e d o r u n n e c e s s a r y t r a n s a c t i o n s ) .
// • T h i n k c a r e f u l l y a b o u t a d d i n g a f u n c t i o n ; t h e r e m i g h t a l r e a d y b e o n e f o r w h a t y o u n e e d .
// • D o c u m e n t t h e e x p e c t e d c a s e s f o r e v e r y t h i n g .
// • E x p r e s s t h o s e c a s e s i n t e s t s .
@objc ( LKSyncMessagesProtocol )
public final class SyncMessagesProtocol : NSObject {
// / O n l y e v e r m o d i f i e d f r o m t h e m e s s a g e p r o c e s s i n g q u e u e ( ` O W S B a t c h M e s s a g e P r o c e s s o r . p r o c e s s i n g Q u e u e ` ) .
private static var syncMessageTimestamps : [ String : Set < UInt64 > ] = [ : ]
internal static var storage : OWSPrimaryStorage { OWSPrimaryStorage . shared ( ) }
// MARK: - S e n d i n g
@objc ( shouldSkipConfigurationSyncMessage )
public static func shouldSkipConfigurationSyncMessage ( ) -> Bool {
// FIXME: W e a d d e d t h i s c h e c k t o a v o i d a c r a s h , b u t w e s h o u l d r e a l l y f i g u r e o u t w h y t h a t c r a s h w a s h a p p e n i n g i n t h e f i r s t p l a c e
return ! UserDefaults . standard [ . hasLaunchedOnce ]
}
@objc ( syncContactWithHexEncodedPublicKey : in : )
public static func syncContact ( _ hexEncodedPublicKey : String , in transaction : YapDatabaseReadTransaction ) {
guard TSContactThread . getWithContactId ( hexEncodedPublicKey , transaction : transaction ) != nil else { return }
let syncManager = SSKEnvironment . shared . syncManager
syncManager . syncContacts ( for : [ SignalAccount ( recipientId : hexEncodedPublicKey ) ] )
}
@objc ( syncAllContacts )
public static func objc_syncAllContacts ( ) -> AnyPromise {
return AnyPromise . from ( syncAllContacts ( ) )
}
public static func syncAllContacts ( ) -> Promise < Void > {
var friends : [ SignalAccount ] = [ ]
TSContactThread . enumerateCollectionObjects { object , _ in
guard let thread = object as ? TSContactThread else { return }
let hexEncodedPublicKey = thread . contactIdentifier ( )
guard thread . isContactFriend && thread . shouldThreadBeVisible && ! thread . isForceHidden else { return }
friends . append ( SignalAccount ( recipientId : hexEncodedPublicKey ) )
}
friends . append ( SignalAccount ( recipientId : getUserHexEncodedPublicKey ( ) ) ) // TODO: W e s u r e a b o u t t h i s ?
let syncManager = SSKEnvironment . shared . syncManager
let promises = friends . chunked ( by : 3 ) . map { friends -> Promise < Void > in // TODO: D o e s t h i s a l w a y s f i t ?
return Promise ( syncManager . syncContacts ( for : friends ) ) . map { _ in }
}
return when ( fulfilled : promises )
}
@objc ( syncAllClosedGroups )
public static func objc_syncAllClosedGroups ( ) -> AnyPromise {
return AnyPromise . from ( syncAllClosedGroups ( ) )
}
public static func syncAllClosedGroups ( ) -> Promise < Void > {
var groups : [ TSGroupThread ] = [ ]
TSGroupThread . enumerateCollectionObjects { object , _ in
guard let group = object as ? TSGroupThread , group . groupModel . groupType = = . closedGroup ,
group . shouldThreadBeVisible , ! group . isForceHidden else { return }
groups . append ( group )
}
let syncManager = SSKEnvironment . shared . syncManager
let promises = groups . map { group -> Promise < Void > in
return Promise ( syncManager . syncGroup ( for : group ) ) . map { _ in }
}
return when ( fulfilled : promises )
}
@objc ( syncAllOpenGroups )
public static func objc_syncAllOpenGroups ( ) -> AnyPromise {
return AnyPromise . from ( syncAllOpenGroups ( ) )
}
public static func syncAllOpenGroups ( ) -> Promise < Void > {
let openGroupSyncMessage = SyncOpenGroupsMessage ( )
let ( promise , seal ) = Promise < Void > . pending ( )
let messageSender = SSKEnvironment . shared . messageSender
messageSender . send ( openGroupSyncMessage , success : {
seal . fulfill ( ( ) )
} , failure : { error in
seal . reject ( error )
} )
return promise
}
// MARK: - R e c e i v i n g
@objc ( isValidSyncMessage : in : )
public static func isValidSyncMessage ( _ envelope : SSKProtoEnvelope , in transaction : YapDatabaseReadTransaction ) -> Bool {
// T h e e n v e l o p e s o u r c e i s s e t d u r i n g U D d e c r y p t i o n
let hexEncodedPublicKey = envelope . source !
let linkedDeviceHexEncodedPublicKeys = LokiDatabaseUtilities . getLinkedDeviceHexEncodedPublicKeys ( for : getUserHexEncodedPublicKey ( ) , in : transaction )
return linkedDeviceHexEncodedPublicKeys . contains ( hexEncodedPublicKey )
}
// TODO: W e s h o u l d p r o b a b l y l o o k a t w h y s y n c m e s s a g e s a r e b e i n g d u p l i c a t e d r a t h e r t h a n d o i n g t h i s
@objc ( isDuplicateSyncMessage : fromHexEncodedPublicKey : )
public static func isDuplicateSyncMessage ( _ protoContent : SSKProtoContent , from hexEncodedPublicKey : String ) -> Bool {
guard let syncMessage = protoContent . syncMessage ? . sent else { return false }
var timestamps : Set < UInt64 > = syncMessageTimestamps [ hexEncodedPublicKey ] ? ? [ ]
let hasTimestamp = syncMessage . timestamp != 0
guard hasTimestamp else { return false }
let result = timestamps . contains ( syncMessage . timestamp )
timestamps . insert ( syncMessage . timestamp )
syncMessageTimestamps [ hexEncodedPublicKey ] = timestamps
return result
}
@objc ( updateProfileFromSyncMessageIfNeeded : wrappedIn : using : )
public static func updateProfileFromSyncMessageIfNeeded ( _ dataMessage : SSKProtoDataMessage , wrappedIn envelope : SSKProtoEnvelope , using transaction : YapDatabaseReadWriteTransaction ) {
// T h e e n v e l o p e s o u r c e i s s e t d u r i n g U D d e c r y p t i o n
let hexEncodedPublicKey = envelope . source !
guard let masterHexEncodedPublicKey = storage . getMasterHexEncodedPublicKey ( for : getUserHexEncodedPublicKey ( ) , in : transaction ) else { return }
let wasSentByMasterDevice = ( masterHexEncodedPublicKey = = hexEncodedPublicKey )
guard wasSentByMasterDevice else { return }
SessionProtocol . updateDisplayNameIfNeeded ( for : masterHexEncodedPublicKey , using : dataMessage , appendingShortID : false , in : transaction )
SessionProtocol . updateProfileKeyIfNeeded ( for : masterHexEncodedPublicKey , using : dataMessage )
}
@objc ( handleClosedGroupUpdatedSyncMessageIfNeeded : using : )
public static func handleClosedGroupUpdatedSyncMessageIfNeeded ( _ transcript : OWSIncomingSentMessageTranscript , using transaction : YapDatabaseReadWriteTransaction ) {
// TODO: T h i s c o d e i s p r e t t y m u c h a d u p l i c a t e o f t h e c o d e i n O W S R e c o r d T r a n s c r i p t J o b
guard let group = transcript . dataMessage . group else { return }
let id = group . id
guard let name = group . name else { return }
let members = group . members
let admins = group . admins
let newGroupThread = TSGroupThread . getOrCreateThread ( withGroupId : id , groupType : . closedGroup , transaction : transaction )
let newGroupModel = TSGroupModel ( title : name , memberIds : members , image : nil , groupId : id , groupType : . closedGroup , adminIds : admins )
let contactsManager = SSKEnvironment . shared . contactsManager
let groupUpdatedMessageDescription = newGroupThread . groupModel . getInfoStringAboutUpdate ( to : newGroupModel , contactsManager : contactsManager )
newGroupThread . groupModel = newGroupModel // TODO: S h o u l d t h i s u s e t h e s e t G r o u p M o d e l m e t h o d o n T S G r o u p T h r e a d ?
newGroupThread . save ( with : transaction )
// T r y t o e s t a b l i s h s e s s i o n s w i t h a l l m e m b e r s f o r w h i c h n o n e e x i s t s y e t w h e n a g r o u p i s c r e a t e d o r u p d a t e d
ClosedGroupsProtocol . establishSessionsIfNeeded ( with : members , in : newGroupThread , using : transaction )
OWSDisappearingMessagesJob . shared ( ) . becomeConsistent ( withDisappearingDuration : transcript . dataMessage . expireTimer , thread : newGroupThread , createdByRemoteRecipientId : nil , createdInExistingGroup : true , transaction : transaction )
let groupUpdatedMessage = TSInfoMessage ( timestamp : NSDate . ows_millisecondTimeStamp ( ) , in : newGroupThread , messageType : . typeGroupUpdate , customMessage : groupUpdatedMessageDescription )
groupUpdatedMessage . save ( with : transaction )
}
@objc ( handleClosedGroupQuitSyncMessageIfNeeded : using : )
public static func handleClosedGroupQuitSyncMessageIfNeeded ( _ transcript : OWSIncomingSentMessageTranscript , using transaction : YapDatabaseReadWriteTransaction ) {
guard let group = transcript . dataMessage . group else { return }
let groupThread = TSGroupThread . getOrCreateThread ( withGroupId : group . id , groupType : . closedGroup , transaction : transaction )
groupThread . leaveGroup ( with : transaction )
let groupQuitMessage = TSInfoMessage ( timestamp : NSDate . ows_millisecondTimeStamp ( ) , in : groupThread , messageType : . typeGroupQuit , customMessage : NSLocalizedString ( " GROUP_YOU_LEFT " , comment : " " ) )
groupQuitMessage . save ( with : transaction )
}
@objc ( handleContactSyncMessageIfNeeded : wrappedIn : using : )
public static func handleContactSyncMessageIfNeeded ( _ syncMessage : SSKProtoSyncMessage , wrappedIn envelope : SSKProtoEnvelope , using transaction : YapDatabaseReadWriteTransaction ) {
// T h e e n v e l o p e s o u r c e i s s e t d u r i n g U D d e c r y p t i o n
let hexEncodedPublicKey = envelope . source !
guard let masterHexEncodedPublicKey = storage . getMasterHexEncodedPublicKey ( for : getUserHexEncodedPublicKey ( ) , in : transaction ) else { return }
let wasSentByMasterDevice = ( masterHexEncodedPublicKey = = hexEncodedPublicKey )
guard wasSentByMasterDevice , let contacts = syncMessage . contacts , let contactsAsData = contacts . data , contactsAsData . count > 0 else { return }
print ( " [Loki] Contact sync message received. " )
handleContactSyncMessageData ( contactsAsData , using : transaction )
}
public static func handleContactSyncMessageData ( _ data : Data , using transaction : YapDatabaseReadWriteTransaction ) {
let parser = ContactParser ( data : data )
let hexEncodedPublicKeys = parser . parseHexEncodedPublicKeys ( )
// T r y t o e s t a b l i s h s e s s i o n s
for hexEncodedPublicKey in hexEncodedPublicKeys {
let thread = TSContactThread . getOrCreateThread ( withContactId : hexEncodedPublicKey , transaction : transaction )
let friendRequestStatus = storage . getFriendRequestStatus ( forContact : hexEncodedPublicKey , transaction : transaction )
switch friendRequestStatus {
case . none :
let messageSender = SSKEnvironment . shared . messageSender
let autoGeneratedFRMessage = MultiDeviceProtocol . getAutoGeneratedMultiDeviceFRMessage ( for : hexEncodedPublicKey , in : transaction )
thread . isForceHidden = true
thread . save ( with : transaction )
// T h i s t a k e s i n t o a c c o u n t m u l t i d e v i c e
messageSender . send ( autoGeneratedFRMessage , success : {
DispatchQueue . main . async {
storage . dbReadWriteConnection . readWrite { transaction in
autoGeneratedFRMessage . remove ( with : transaction )
thread . isForceHidden = false
thread . save ( with : transaction )
}
}
} , failure : { error in
DispatchQueue . main . async {
storage . dbReadWriteConnection . readWrite { transaction in
autoGeneratedFRMessage . remove ( with : transaction )
thread . isForceHidden = false
thread . save ( with : transaction )
}
}
} )
case . requestReceived :
storage . setFriendRequestStatus ( . friends , forContact : hexEncodedPublicKey , transaction : transaction )
// N o t s e n d F r i e n d R e q u e s t A c c e p t a n c e M e s s a g e ( t o : i n : u s i n g : ) t o t a k e i n t o a c c o u n t m u l t i d e v i c e
FriendRequestProtocol . acceptFriendRequest ( in : thread , using : transaction )
default : break
}
}
}
@objc ( handleClosedGroupSyncMessageIfNeeded : wrappedIn : using : )
public static func handleClosedGroupSyncMessageIfNeeded ( _ syncMessage : SSKProtoSyncMessage , wrappedIn envelope : SSKProtoEnvelope , using transaction : YapDatabaseReadWriteTransaction ) {
// T h e e n v e l o p e s o u r c e i s s e t d u r i n g U D d e c r y p t i o n
let hexEncodedPublicKey = envelope . source !
guard let masterHexEncodedPublicKey = storage . getMasterHexEncodedPublicKey ( for : getUserHexEncodedPublicKey ( ) , in : transaction ) else { return }
let wasSentByMasterDevice = ( masterHexEncodedPublicKey = = hexEncodedPublicKey )
guard wasSentByMasterDevice , let groups = syncMessage . groups , let groupsAsData = groups . data , groupsAsData . count > 0 else { return }
print ( " [Loki] Closed group sync message received. " )
let parser = ClosedGroupParser ( data : groupsAsData )
let groupModels = parser . parseGroupModels ( )
for groupModel in groupModels {
var thread : TSGroupThread ! = TSGroupThread ( groupId : groupModel . groupId , transaction : transaction )
if thread = = nil {
thread = TSGroupThread . getOrCreateThread ( with : groupModel , transaction : transaction )
thread . save ( with : transaction )
}
ClosedGroupsProtocol . establishSessionsIfNeeded ( with : groupModel . groupMemberIds , in : thread , using : transaction )
let infoMessage = TSInfoMessage ( timestamp : NSDate . ows_millisecondTimeStamp ( ) , in : thread , messageType : . typeGroupUpdate , customMessage : " You have joined the group. " )
infoMessage . save ( with : transaction )
}
}
@objc ( handleOpenGroupSyncMessageIfNeeded : wrappedIn : using : )
public static func handleOpenGroupSyncMessageIfNeeded ( _ syncMessage : SSKProtoSyncMessage , wrappedIn envelope : SSKProtoEnvelope , using transaction : YapDatabaseReadTransaction ) {
// T h e e n v e l o p e s o u r c e i s s e t d u r i n g U D d e c r y p t i o n
let hexEncodedPublicKey = envelope . source !
guard let masterHexEncodedPublicKey = storage . getMasterHexEncodedPublicKey ( for : getUserHexEncodedPublicKey ( ) , in : transaction ) else { return }
let wasSentByMasterDevice = ( masterHexEncodedPublicKey = = hexEncodedPublicKey )
guard wasSentByMasterDevice else { return }
let groups = syncMessage . openGroups
guard groups . count > 0 else { return }
print ( " [Loki] Open group sync message received. " )
for openGroup in groups {
LokiPublicChatManager . shared . addChat ( server : openGroup . url , channel : openGroup . channel )
}
}
}