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 > {
// C o l l e c t a l l m a s t e r d e v i c e s w i t h w h i c h a s e s s i o n h a s b e e n e s t a b l i s h e d . N o t e t h a t
// t h i s i s n o t t h e s a m e a s a l l m a s t e r d e v i c e s w i t h w h i c h w e ' r e f r i e n d s .
var hepks : Set < String > = [ ]
TSContactThread . enumerateCollectionObjects { object , _ in
guard let thread = object as ? TSContactThread else { return }
let hexEncodedPublicKey = thread . contactIdentifier ( )
guard thread . isContactFriend && thread . shouldThreadBeVisible && ! thread . isSlaveThread else { return }
hepks . insert ( hexEncodedPublicKey )
}
TSGroupThread . enumerateCollectionObjects { object , _ in
guard let group = object as ? TSGroupThread , group . groupModel . groupType = = . closedGroup ,
group . shouldThreadBeVisible else { return }
hepks . formUnion ( group . groupModel . groupMemberIds )
}
hepks . insert ( getUserHexEncodedPublicKey ( ) ) // TODO: A r e w e s u r e a b o u t t h i s ?
let friends = hepks . map { SignalAccount ( recipientId : $0 ) }
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 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 }
SessionMetaProtocol . updateDisplayNameIfNeeded ( for : masterHexEncodedPublicKey , using : dataMessage , appendingShortID : false , in : transaction )
SessionMetaProtocol . 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 )
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 !
let linkedDevices = LokiDatabaseUtilities . getLinkedDeviceHexEncodedPublicKeys ( for : hexEncodedPublicKey , in : transaction )
let wasSentByLinkedDevice = linkedDevices . contains ( hexEncodedPublicKey )
guard wasSentByLinkedDevice , 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 ( )
let linkedDevices = LokiDatabaseUtilities . getLinkedDeviceHexEncodedPublicKeys ( for : getUserHexEncodedPublicKey ( ) , in : transaction )
// 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 {
guard ! linkedDevices . contains ( hexEncodedPublicKey ) else { continue } // S k i p s e l f
// W e d o n ' t u p d a t e t h e f r i e n d r e q u e s t s t a t u s ; t h a t ' s d o n e i n O W S M e s s a g e S e n d e r . s e n d M e s s a g e ( _ : )
let friendRequestStatus = storage . getFriendRequestStatus ( for : hexEncodedPublicKey , transaction : transaction )
switch friendRequestStatus {
case . none , . requestExpired :
let messageSender = SSKEnvironment . shared . messageSender
// W e n e e d t o s e n d t h e F R m e s s a g e t o a l l o f t h e u s e r ' s d e v i c e s a s t h e c o n t a c t s y n c m e s s a g e e x c l u d e s s l a v e d e v i c e s
let autoGeneratedFRMessage = MultiDeviceProtocol . getAutoGeneratedMultiDeviceFRMessage ( for : hexEncodedPublicKey , in : 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 )
}
}
} , failure : { error in
DispatchQueue . main . async {
storage . dbReadWriteConnection . readWrite { transaction in
autoGeneratedFRMessage . remove ( with : transaction )
}
}
} )
case . requestReceived :
// 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 : 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 ( from : hexEncodedPublicKey , using : transaction )
// I t ' s i m p o r t a n t t h a t t h e l i n e b e l o w h a p p e n s a f t e r t h e o n e a b o v e
storage . setFriendRequestStatus ( . friends , for : hexEncodedPublicKey , transaction : 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 !
let linkedDevices = LokiDatabaseUtilities . getLinkedDeviceHexEncodedPublicKeys ( for : hexEncodedPublicKey , in : transaction )
let wasSentByLinkedDevice = linkedDevices . contains ( hexEncodedPublicKey )
guard wasSentByLinkedDevice , 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 )
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 !
let linkedDevices = LokiDatabaseUtilities . getLinkedDeviceHexEncodedPublicKeys ( for : hexEncodedPublicKey , in : transaction )
let wasSentByLinkedDevice = linkedDevices . contains ( hexEncodedPublicKey )
guard wasSentByLinkedDevice else { return }
let groups = syncMessage . openGroups
guard groups . count > 0 else { return }
print ( " [Loki] Open group sync message received. " )
for openGroup in groups {
let openGroupManager = LokiPublicChatManager . shared
guard openGroupManager . getChat ( server : openGroup . url , channel : openGroup . channel ) = = nil else { return }
openGroupManager . addChat ( server : openGroup . url , channel : openGroup . channel )
}
}
}