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 ( LKMultiDeviceProtocol )
public final class MultiDeviceProtocol : NSObject {
private static var _lastDeviceLinkUpdate : [ String : Date ] = [ : ]
// / A m a p p i n g f r o m h e x e n c o d e d p u b l i c k e y t o d a t e u p d a t e d .
public static var lastDeviceLinkUpdate : [ String : Date ] {
get { LokiAPI . stateQueue . sync { _lastDeviceLinkUpdate } }
set { LokiAPI . stateQueue . sync { _lastDeviceLinkUpdate = newValue } }
}
// TODO: I d o n ' t t h i n k s t a t e Q u e u e a c t u a l l y h e l p s a v o i d r a c e c o n d i t i o n s
internal static var storage : OWSPrimaryStorage { OWSPrimaryStorage . shared ( ) }
// MARK: - S e t t i n g s
public static let deviceLinkUpdateInterval : TimeInterval = 20
// MARK: - M u l t i D e v i c e D e s t i n a t i o n
public struct MultiDeviceDestination : Hashable {
public let hexEncodedPublicKey : String
public let isMaster : Bool
}
// MARK: - I n i t i a l i z a t i o n
private override init ( ) { }
// MARK: - S e n d i n g ( P a r t 1 )
@objc ( isMultiDeviceRequiredForMessage : )
public static func isMultiDeviceRequired ( for message : TSOutgoingMessage ) -> Bool {
return ! ( message is DeviceLinkMessage ) && ! message . thread . isGroupThread ( )
}
private static func copy ( _ messageSend : OWSMessageSend , for destination : MultiDeviceDestination , with seal : Resolver < Void > ) -> OWSMessageSend {
var recipient : SignalRecipient !
storage . dbReadConnection . read { transaction in
recipient = SignalRecipient . getOrBuildUnsavedRecipient ( forRecipientId : destination . hexEncodedPublicKey , transaction : transaction )
}
// TODO: W h y i s i t o k a y t h a t t h e t h r e a d , s e n d e r c e r t i f i c a t e , e t c . d o n ' t g e t c h a n g e d ?
return OWSMessageSend ( message : messageSend . message , thread : messageSend . thread , recipient : recipient ,
senderCertificate : messageSend . senderCertificate , udAccess : messageSend . udAccess , localNumber : messageSend . localNumber , success : {
seal . fulfill ( ( ) )
} , failure : { error in
seal . reject ( error )
} )
}
private static func sendMessage ( _ messageSend : OWSMessageSend , to destination : MultiDeviceDestination , in transaction : YapDatabaseReadTransaction ) -> Promise < Void > {
let ( threadPromise , threadPromiseSeal ) = Promise < TSContactThread > . pending ( )
if let thread = TSContactThread . getWithContactId ( destination . hexEncodedPublicKey , transaction : transaction ) {
threadPromiseSeal . fulfill ( thread )
} else {
// D i s p a t c h a s y n c o n t h e m a i n q u e u e t o a v o i d n e s t e d w r i t e t r a n s a c t i o n s
DispatchQueue . main . async {
storage . dbReadWriteConnection . readWrite { transaction in
let thread = TSContactThread . getOrCreateThread ( withContactId : destination . hexEncodedPublicKey , transaction : transaction )
threadPromiseSeal . fulfill ( thread )
}
}
}
return threadPromise . then ( on : OWSDispatch . sendingQueue ( ) ) { thread -> Promise < Void > in
let message = messageSend . message
let messageSender = SSKEnvironment . shared . messageSender
let ( promise , seal ) = Promise < Void > . pending ( )
let shouldSendAutoGeneratedFR = ! thread . isContactFriend && ! ( message is FriendRequestMessage )
&& ! SessionMetaProtocol . isThreadNoteToSelf ( thread ) && message . shouldBeSaved ( ) // s h o u l d B e S a v e d i n d i c a t e s i t i s n ' t a t r a n s i e n t m e s s a g e
if ! shouldSendAutoGeneratedFR {
let messageSendCopy = copy ( messageSend , for : destination , with : seal )
messageSender . sendMessage ( messageSendCopy )
} else {
// D i s p a t c h a s y n c o n t h e m a i n q u e u e t o a v o i d n e s t e d w r i t e t r a n s a c t i o n s
DispatchQueue . main . async {
storage . dbReadWriteConnection . readWrite { transaction in
getAutoGeneratedMultiDeviceFRMessageSend ( for : destination . hexEncodedPublicKey , in : transaction , seal : seal )
. done ( on : OWSDispatch . sendingQueue ( ) ) { autoGeneratedFRMessageSend in
messageSender . sendMessage ( autoGeneratedFRMessageSend )
}
}
}
}
return promise
}
}
// / S e e [ M u l t i D e v i c e M e s s a g e S e n d i n g ] ( h t t p s : / / g i t h u b . c o m / l o k i - p r o j e c t / s e s s i o n - p r o t o c o l - d o c s / w i k i / M u l t i - D e v i c e - M e s s a g e - S e n d i n g ) f o r m o r e i n f o r m a t i o n .
@objc ( sendMessageToDestinationAndLinkedDevices : in : )
public static func sendMessageToDestinationAndLinkedDevices ( _ messageSend : OWSMessageSend , in transaction : YapDatabaseReadTransaction ) {
let message = messageSend . message
let messageSender = SSKEnvironment . shared . messageSender
if ! isMultiDeviceRequired ( for : message ) {
print ( " [Loki] sendMessageToDestinationAndLinkedDevices(_:in:) invoked for a message that doesn't require multi device routing. " )
OWSDispatch . sendingQueue ( ) . async {
messageSender . sendMessage ( messageSend )
}
return
}
print ( " [Loki] Sending \( type ( of : message ) ) message using multi device routing. " )
let recipientID = messageSend . recipient . recipientId ( )
getMultiDeviceDestinations ( for : recipientID , in : transaction ) . done ( on : OWSDispatch . sendingQueue ( ) ) { destinations in
var promises : [ Promise < Void > ] = [ ]
let masterDestination = destinations . first { $0 . isMaster }
if let masterDestination = masterDestination {
storage . dbReadConnection . read { transaction in
promises . append ( sendMessage ( messageSend , to : masterDestination , in : transaction ) )
}
}
let slaveDestinations = destinations . filter { ! $0 . isMaster }
slaveDestinations . forEach { slaveDestination in
storage . dbReadConnection . read { transaction in
promises . append ( sendMessage ( messageSend , to : slaveDestination , in : transaction ) )
}
}
when ( resolved : promises ) . done ( on : OWSDispatch . sendingQueue ( ) ) { results in
let errors = results . compactMap { result -> Error ? in
if case Result . rejected ( let error ) = result {
return error
} else {
return nil
}
}
if errors . isEmpty {
messageSend . success ( )
} else {
messageSend . failure ( errors . first ! )
}
}
} . catch ( on : OWSDispatch . sendingQueue ( ) ) { error in
// P r o c e e d e v e n i f u p d a t i n g t h e r e c i p i e n t ' s d e v i c e l i n k s f a i l e d , s o t h a t m e s s a g e s e n d i n g
// i s i n d e p e n d e n t o f w h e t h e r t h e f i l e s e r v e r i s o n l i n e
messageSender . sendMessage ( messageSend )
}
}
@objc ( updateDeviceLinksIfNeededForHexEncodedPublicKey : in : )
public static func updateDeviceLinksIfNeeded ( for hexEncodedPublicKey : String , in transaction : YapDatabaseReadTransaction ) -> AnyPromise {
let promise = getMultiDeviceDestinations ( for : hexEncodedPublicKey , in : transaction )
return AnyPromise . from ( promise )
}
// / S e e [ A u t o - G e n e r a t e d F r i e n d R e q u e s t s ] ( h t t p s : / / g i t h u b . c o m / l o k i - p r o j e c t / s e s s i o n - p r o t o c o l - d o c s / w i k i / A u t o - G e n e r a t e d - F r i e n d - R e q u e s t s ) f o r m o r e i n f o r m a t i o n .
@objc ( getAutoGeneratedMultiDeviceFRMessageForHexEncodedPublicKey : in : )
public static func getAutoGeneratedMultiDeviceFRMessage ( for hexEncodedPublicKey : String , in transaction : YapDatabaseReadWriteTransaction ) -> FriendRequestMessage {
let thread = TSContactThread . getOrCreateThread ( withContactId : hexEncodedPublicKey , transaction : transaction )
let result = FriendRequestMessage ( timestamp : NSDate . ows_millisecondTimeStamp ( ) , thread : thread , body : " Please accept to enable messages to be synced across devices " )
result . skipSave = true // TODO: W h y i s t h i s n e c e s s a r y a g a i n ?
return result
}
// / S e e [ A u t o - G e n e r a t e d F r i e n d R e q u e s t s ] ( h t t p s : / / g i t h u b . c o m / l o k i - p r o j e c t / s e s s i o n - p r o t o c o l - d o c s / w i k i / A u t o - G e n e r a t e d - F r i e n d - R e q u e s t s ) f o r m o r e i n f o r m a t i o n .
@objc ( getAutoGeneratedMultiDeviceFRMessageSendForHexEncodedPublicKey : in : )
public static func objc_getAutoGeneratedMultiDeviceFRMessageSend ( for hexEncodedPublicKey : String , in transaction : YapDatabaseReadWriteTransaction ) -> AnyPromise {
return AnyPromise . from ( getAutoGeneratedMultiDeviceFRMessageSend ( for : hexEncodedPublicKey , in : transaction ) )
}
// / S e e [ A u t o - G e n e r a t e d F r i e n d R e q u e s t s ] ( h t t p s : / / g i t h u b . c o m / l o k i - p r o j e c t / s e s s i o n - p r o t o c o l - d o c s / w i k i / A u t o - G e n e r a t e d - F r i e n d - R e q u e s t s ) f o r m o r e i n f o r m a t i o n .
public static func getAutoGeneratedMultiDeviceFRMessageSend ( for hexEncodedPublicKey : String , in transaction : YapDatabaseReadWriteTransaction , seal externalSeal : Resolver < Void > ? = nil ) -> Promise < OWSMessageSend > {
// 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 thread = TSContactThread . getOrCreateThread ( withContactId : hexEncodedPublicKey , transaction : transaction )
let message = getAutoGeneratedMultiDeviceFRMessage ( for : hexEncodedPublicKey , in : transaction )
let recipient = SignalRecipient . getOrBuildUnsavedRecipient ( forRecipientId : hexEncodedPublicKey , transaction : transaction )
let udManager = SSKEnvironment . shared . udManager
let senderCertificate = udManager . getSenderCertificate ( )
let ( promise , seal ) = Promise < OWSMessageSend > . pending ( )
// D i s p a t c h a s y n c o n t h e m a i n q u e u e t o a v o i d n e s t e d w r i t e t r a n s a c t i o n s
DispatchQueue . main . async {
var recipientUDAccess : OWSUDAccess ?
if let senderCertificate = senderCertificate {
recipientUDAccess = udManager . udAccess ( forRecipientId : hexEncodedPublicKey , requireSyncAccess : true ) // S t a r t s a n e w w r i t e t r a n s a c t i o n i n t e r n a l l y
}
let messageSend = OWSMessageSend ( message : message , thread : thread , recipient : recipient , senderCertificate : senderCertificate ,
udAccess : recipientUDAccess , localNumber : getUserHexEncodedPublicKey ( ) , success : {
externalSeal ? . fulfill ( ( ) )
} , failure : { error in
externalSeal ? . reject ( error )
} )
seal . fulfill ( messageSend )
}
return promise
}
// MARK: - R e c e i v i n g
@objc ( handleDeviceLinkMessageIfNeeded : wrappedIn : using : )
public static func handleDeviceLinkMessageIfNeeded ( _ protoContent : SSKProtoContent , 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 deviceLinkMessage = protoContent . lokiDeviceLinkMessage , let master = deviceLinkMessage . masterHexEncodedPublicKey ,
let slave = deviceLinkMessage . slaveHexEncodedPublicKey , let slaveSignature = deviceLinkMessage . slaveSignature else {
print ( " [Loki] Received an invalid device link message. " )
return
}
let deviceLinkingSession = DeviceLinkingSession . current
if let masterSignature = deviceLinkMessage . masterSignature { // A u t h o r i z a t i o n
print ( " [Loki] Received a device link authorization from: \( hexEncodedPublicKey ) . " ) // I n t e n t i o n a l l y n o t ` m a s t e r `
if let deviceLinkingSession = deviceLinkingSession {
deviceLinkingSession . processLinkingAuthorization ( from : master , for : slave , masterSignature : masterSignature , slaveSignature : slaveSignature )
} else {
print ( " [Loki] Received a device link authorization without a session; ignoring. " )
}
// S e t a n y p r o f i l e i n f o ( t h e d e v i c e l i n k a u t h o r i z a t i o n a l s o i n c l u d e s t h e m a s t e r d e v i c e ' s p r o f i l e i n f o )
if let dataMessage = protoContent . dataMessage {
SessionMetaProtocol . updateDisplayNameIfNeeded ( for : master , using : dataMessage , appendingShortID : false , in : transaction )
SessionMetaProtocol . updateProfileKeyIfNeeded ( for : master , using : dataMessage )
}
} else { // R e q u e s t
print ( " [Loki] Received a device link request from: \( hexEncodedPublicKey ) . " ) // I n t e n t i o n a l l y n o t ` s l a v e `
if let deviceLinkingSession = deviceLinkingSession {
deviceLinkingSession . processLinkingRequest ( from : slave , to : master , with : slaveSignature )
} else {
NotificationCenter . default . post ( name : . unexpectedDeviceLinkRequestReceived , object : nil )
}
}
}
@objc ( isUnlinkDeviceMessage : )
public static func isUnlinkDeviceMessage ( _ dataMessage : SSKProtoDataMessage ) -> Bool {
let unlinkDeviceFlag = SSKProtoDataMessage . SSKProtoDataMessageFlags . unlinkDevice
return dataMessage . flags & UInt32 ( unlinkDeviceFlag . rawValue ) != 0
}
@objc ( handleUnlinkDeviceMessage : wrappedIn : using : )
public static func handleUnlinkDeviceMessage ( _ 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 !
// C h e c k t h a t t h e r e q u e s t w a s s e n t b y o u r m a s t e r d e v i c e
guard let masterHexEncodedPublicKey = storage . getMasterHexEncodedPublicKey ( for : getUserHexEncodedPublicKey ( ) , in : transaction ) else { return }
let wasSentByMasterDevice = ( masterHexEncodedPublicKey = = hexEncodedPublicKey )
guard wasSentByMasterDevice else { return }
// I g n o r e t h e r e q u e s t i f w e d o n ' t k n o w a b o u t t h e d e v i c e l i n k i n q u e s t i o n
let masterDeviceLinks = storage . getDeviceLinks ( for : masterHexEncodedPublicKey , in : transaction )
if ! masterDeviceLinks . contains ( where : {
$0 . master . hexEncodedPublicKey = = masterHexEncodedPublicKey && $0 . slave . hexEncodedPublicKey = = getUserHexEncodedPublicKey ( )
} ) {
return
}
LokiFileServerAPI . getDeviceLinks ( associatedWith : getUserHexEncodedPublicKey ( ) ) . done ( on : DispatchQueue . main ) { slaveDeviceLinks in
// C h e c k t h a t t h e d e v i c e l i n k I S p r e s e n t o n t h e f i l e s e r v e r .
// N o t e t h a t t h e d e v i c e l i n k a s s e e n f r o m t h e m a s t e r d e v i c e ' s p e r s p e c t i v e h a s b e e n d e l e t e d a t t h i s p o i n t , b u t t h e
// d e v i c e l i n k a s s e e n f r o m t h e s l a v e p e r s p e c t i v e h a s n ' t .
if slaveDeviceLinks . contains ( where : {
$0 . master . hexEncodedPublicKey = = masterHexEncodedPublicKey && $0 . slave . hexEncodedPublicKey = = getUserHexEncodedPublicKey ( )
} ) {
for deviceLink in slaveDeviceLinks { // I n t h e o r y t h e r e s h o u l d o n l y b e o n e
LokiFileServerAPI . removeDeviceLink ( deviceLink ) // A t t e m p t t o c l e a n u p o n t h e f i l e s e r v e r
}
UserDefaults . standard [ . wasUnlinked ] = true
NotificationCenter . default . post ( name : . dataNukeRequested , object : nil )
}
}
}
// MARK: - G e n e r a l
@objc public static func isSlaveThread ( _ thread : TSThread ) -> Bool {
guard let thread = thread as ? TSContactThread else { return false }
var isSlaveThread = false
storage . dbReadConnection . read { transaction in
isSlaveThread = storage . getMasterHexEncodedPublicKey ( for : thread . contactIdentifier ( ) , in : transaction ) != nil
}
return isSlaveThread
}
}
// MARK: - S e n d i n g ( P a r t 2 )
// H e r e ( i n a n o n - @ o b j c e x t e n s i o n ) b e c a u s e i t d o e s n ' t i n t e r o p e r a t e w e l l w i t h O b j - C
public extension MultiDeviceProtocol {
fileprivate static func getMultiDeviceDestinations ( for hexEncodedPublicKey : String , in transaction : YapDatabaseReadTransaction ) -> Promise < Set < MultiDeviceDestination > > {
let ( promise , seal ) = Promise < Set < MultiDeviceDestination > > . pending ( )
func getDestinations ( in transaction : YapDatabaseReadTransaction ? = nil ) {
storage . dbReadConnection . read { transaction in
var destinations : Set < MultiDeviceDestination > = [ ]
let masterHexEncodedPublicKey = storage . getMasterHexEncodedPublicKey ( for : hexEncodedPublicKey , in : transaction ) ? ? hexEncodedPublicKey
let masterDestination = MultiDeviceDestination ( hexEncodedPublicKey : masterHexEncodedPublicKey , isMaster : true )
destinations . insert ( masterDestination )
let deviceLinks = storage . getDeviceLinks ( for : masterHexEncodedPublicKey , in : transaction )
let slaveDestinations = deviceLinks . map { MultiDeviceDestination ( hexEncodedPublicKey : $0 . slave . hexEncodedPublicKey , isMaster : false ) }
destinations . formUnion ( slaveDestinations )
seal . fulfill ( destinations )
}
}
let timeSinceLastUpdate : TimeInterval
if let lastDeviceLinkUpdate = lastDeviceLinkUpdate [ hexEncodedPublicKey ] {
timeSinceLastUpdate = Date ( ) . timeIntervalSince ( lastDeviceLinkUpdate )
} else {
timeSinceLastUpdate = . infinity
}
if timeSinceLastUpdate > deviceLinkUpdateInterval {
let masterHexEncodedPublicKey = storage . getMasterHexEncodedPublicKey ( for : hexEncodedPublicKey , in : transaction ) ? ? hexEncodedPublicKey
LokiFileServerAPI . getDeviceLinks ( associatedWith : masterHexEncodedPublicKey ) . done ( on : LokiAPI . workQueue ) { _ in
getDestinations ( )
lastDeviceLinkUpdate [ hexEncodedPublicKey ] = Date ( )
} . catch ( on : LokiAPI . workQueue ) { error in
if ( error as ? LokiDotNetAPI . LokiDotNetAPIError ) = = LokiDotNetAPI . LokiDotNetAPIError . parsingFailed {
// D o n ' t i m m e d i a t e l y r e - f e t c h i n c a s e o f f a i l u r e d u e t o a p a r s i n g e r r o r
lastDeviceLinkUpdate [ hexEncodedPublicKey ] = Date ( )
getDestinations ( )
} else {
print ( " [Loki] Failed to get device links due to error: \( error ) . " )
seal . reject ( error )
}
}
} else {
getDestinations ( )
}
return promise
}
}