@ -68,15 +68,18 @@ import org.thoughtcrime.securesms.linkpreview.Link;
import org.thoughtcrime.securesms.linkpreview.LinkPreview ;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil ;
import org.thoughtcrime.securesms.logging.Log ;
import org.thoughtcrime.securesms.loki.FriendRequestHandler ;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase ;
import org.thoughtcrime.securesms.loki.protocol.LokiSessionResetImplementation ;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase ;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities ;
import org.thoughtcrime.securesms.loki.activities.HomeActivity ;
import org.thoughtcrime.securesms.loki. utilities.MentionManagerUtilities ;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase ;
import org.thoughtcrime.securesms.loki.database.LokiPreKeyBundleDatabase ;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase ;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol ;
import org.thoughtcrime.securesms.loki.protocol.FriendRequestProtocol ;
import org.thoughtcrime.securesms.loki.protocol.LokiSessionResetImplementation ;
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol ;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol ;
import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol ;
import org.thoughtcrime.securesms.loki.utilities.Broadcaster ;
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities ;
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities ;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage ;
import org.thoughtcrime.securesms.mms.MmsException ;
@ -144,7 +147,7 @@ import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager;
import org.whispersystems.signalservice.loki.protocol.meta.LokiServiceMessage ;
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink ;
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLinkingSession ;
import org.whispersystems.signalservice.loki.protocol. multidevice.LokiDeviceLinkUtilities ;
import org.whispersystems.signalservice.loki.protocol. sessionmanagement.SessionManagementProtocol ;
import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus ;
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus ;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil ;
@ -285,9 +288,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
SignalServiceContent content = cipher . decrypt ( envelope ) ;
// Loki - Ignore any friend requests that we got before restoration
if ( content. isFriendRequest ( ) & & content . getTimestamp ( ) < TextSecurePreferences . getRestorationTime ( contex t) ) {
Log . d ( "Loki" , "Ignoring friend request received before restoration.") ;
// Loki - Ignore any friend requests from before restoration
if ( FriendRequestProtocol. isFriendRequestFromBeforeRestoration ( conten t) ) {
Log . d ( "Loki" , "Ignoring friend request from before restoration.") ;
return ;
}
@ -297,18 +300,15 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
// Loki - Handle friend request acceptance if needed
if ( ! content . isFriendRequest ( ) & & ! isGroupChatMessage ( content ) ) {
becomeFriendsWithContactIfNeeded ( content . getSender ( ) , true , false ) ;
}
FriendRequestProtocol . handleFriendRequestAcceptanceIfNeeded ( content ) ;
// Loki - Handle session request if needed
handleSessionRequest IfNeeded( content ) ;
// Loki - Handle pre key bundle message if needed
SessionManagementProtocol. handlePreKeyBundleMessage IfNeeded( content ) ;
// Loki - Store pre key bundle if needed
if ( ! content . getDeviceLink ( ) . isPresent ( ) ) {
storePreKeyBundleIfNeeded ( content ) ;
}
// Loki - Handle session request if needed
SessionManagementProtocol . handleSessionRequestIfNeeded ( content ) ;
// Loki - Handle address message if needed
if ( content . lokiServiceMessage . isPresent ( ) ) {
LokiServiceMessage lokiMessage = content . lokiServiceMessage . get ( ) ;
if ( lokiMessage . getAddressMessage ( ) ! = null ) {
@ -316,47 +316,33 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
// Loki - Store the sender display name if needed
Optional < String > rawSenderDisplayName = content . senderDisplayName ;
if ( rawSenderDisplayName . isPresent ( ) & & rawSenderDisplayName . get ( ) . length ( ) > 0 ) {
// If we got a name from our master device then set our display name to match
String ourMasterDevice = TextSecurePreferences . getMasterHexEncodedPublicKey ( context ) ;
if ( ourMasterDevice ! = null & & content . getSender ( ) . equals ( ourMasterDevice ) ) {
TextSecurePreferences . setProfileName ( context , rawSenderDisplayName . get ( ) ) ;
}
// If we receive a message from our device then don't set the display name in the database (as we probably have a alias set for them)
MultiDeviceUtilities . isOneOfOurDevices ( context , Address . fromSerialized ( content . getSender ( ) ) ) . success ( isOneOfOurDevices - > {
if ( ! isOneOfOurDevices ) { setDisplayName ( content . getSender ( ) , rawSenderDisplayName . get ( ) ) ; }
return Unit . INSTANCE ;
} ) ;
}
// Loki - Handle profile update if needed
SessionMetaProtocol . handleProfileUpdateIfNeeded ( content ) ;
if ( content . getDeviceLink ( ) . isPresent ( ) ) {
handleDeviceLinkMessage( content . getDeviceLink ( ) . get ( ) , content ) ;
MultiDeviceProtocol . handleDeviceLinkMessageIfNeeded ( content ) ;
} else if ( content . getDataMessage ( ) . isPresent ( ) ) {
SignalServiceDataMessage message = content . getDataMessage ( ) . get ( ) ;
boolean isMediaMessage = message . getAttachments ( ) . isPresent ( ) | | message . getQuote ( ) . isPresent ( ) | | message . getSharedContacts ( ) . isPresent ( ) | | message . getPreviews ( ) . isPresent ( ) | | message . getSticker ( ) . isPresent ( ) ;
if ( ! content . isFriendRequest ( ) & & message . isUnlinkingRequest ( ) ) {
// Make sure we got the request from our master device
String ourMasterDevice = TextSecurePreferences . getMasterHexEncodedPublicKey ( context ) ;
if ( ourMasterDevice ! = null & & ourMasterDevice . equals ( content . getSender ( ) ) ) {
TextSecurePreferences . setNeedDatabaseResetFromUnlink ( context , true ) ;
MultiDeviceUtilities . checkIsRevokedSlaveDevice ( context ) ;
}
// Loki - Handle unlinking request if needed
if ( message . isUnlinkingRequest ( ) ) {
MultiDeviceProtocol . handleUnlinkingRequest ( message ) ;
} else {
// Loki - Don't process session restor e message any further
// Loki - Don't process session restoration requests or session requests any further
if ( message . isSessionRestorationRequest ( ) | | message . isSessionRequest ( ) ) { return ; }
if ( message . isEndSession ( ) ) handleEndSessionMessage ( content , smsMessageId ) ;
else if ( message . isGroupUpdate ( ) ) handleGroupMessage ( content , message , smsMessageId ) ;
else if ( message . isExpirationUpdate ( ) )
if ( message . isEndSession ( ) ) {
handleEndSessionMessage ( content , smsMessageId ) ;
} else if ( message . isGroupUpdate ( ) ) {
handleGroupMessage ( content , message , smsMessageId ) ;
} else if ( message . isExpirationUpdate ( ) ) {
handleExpirationUpdate ( content , message , smsMessageId ) ;
else if ( isMediaMessage )
} else if ( isMediaMessage ) {
handleMediaMessage ( content , message , smsMessageId , Optional . absent ( ) ) ;
else if ( message . getBody ( ) . isPresent ( ) )
} else if ( message . getBody ( ) . isPresent ( ) ) {
handleTextMessage ( content , message , smsMessageId , Optional . absent ( ) ) ;
}
if ( message . getGroupInfo ( ) . isPresent ( ) & & groupDatabase . isUnknownGroup ( GroupUtil . getEncodedId ( message . getGroupInfo ( ) . get ( ) ) ) ) {
handleUnknownGroupMessage ( content , message . getGroupInfo ( ) . get ( ) ) ;
@ -366,25 +352,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
handleProfileKey ( content , message ) ;
}
// Loki - This doesn't get invoked for group chats
if ( content . isNeedsReceipt ( ) ) {
handleNeedsDeliveryReceipt ( content , message ) ;
}
// If we received a friend request, but we were already friends with the user, reset the session
if ( content . isFriendRequest ( ) & & ! message . isGroupMessage ( ) ) {
Recipient sender = Recipient . from ( context , Address . fromSerialized ( content . getSender ( ) ) , false ) ;
ThreadDatabase threadDatabase = DatabaseFactory . getThreadDatabase ( context ) ;
long threadID = threadDatabase . getThreadIdIfExistsFor ( sender ) ;
if ( lokiThreadDatabase . getFriendRequestStatus ( threadID ) = = LokiThreadFriendRequestStatus . FRIENDS ) {
resetSession ( content . getSender ( ) ) ;
// Let our other devices know that we have reset the session
MessageSender . syncContact ( context , sender . getAddress ( ) ) ;
}
}
// Loki - Handle friend request logic if needed
updateFriendRequestStatusIfNeeded ( content , message ) ;
// Loki - Handle friend request message if needed
FriendRequestProtocol . handleFriendRequestMessageIfNeeded ( content ) ;
}
} else if ( content . getSyncMessage ( ) . isPresent ( ) ) {
TextSecurePreferences . setMultiDevice ( context , true ) ;
@ -557,26 +530,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
if ( threadId ! = null ) {
resetSession( content . getSender ( ) ) ;
SessionManagementProtocol. handleEndSessionMessage ( content ) ;
MessageNotifier . updateNotification ( context , threadId ) ;
}
}
private void resetSession ( String hexEncodedPublicKey ) {
TextSecureSessionStore sessionStore = new TextSecureSessionStore ( context ) ;
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory . getLokiThreadDatabase ( context ) ;
Log . d ( "Loki" , "Received a session reset request from: " + hexEncodedPublicKey + "; archiving the session." ) ;
sessionStore . archiveAllSessions ( hexEncodedPublicKey ) ;
lokiThreadDatabase . setSessionResetStatus ( hexEncodedPublicKey , LokiSessionResetStatus . REQUEST_RECEIVED ) ;
Log . d ( "Loki" , "Sending a ping back to " + hexEncodedPublicKey + "." ) ;
MessageSender . sendBackgroundMessage ( context , hexEncodedPublicKey ) ;
SecurityEvent . broadcastSecurityUpdateEvent ( context ) ;
}
private long handleSynchronizeSentEndSessionMessage ( @NonNull SentTranscriptMessage message )
{
SmsDatabase database = DatabaseFactory . getSmsDatabase ( context ) ;
@ -688,92 +646,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
private void handleContactSyncMessage ( @NonNull ContactsMessage contactsMessage ) {
if ( ! contactsMessage . getContactsStream ( ) . isStream ( ) ) { return ; }
Log . d ( "Loki" , "Received contact sync message." ) ;
try {
InputStream in = contactsMessage . getContactsStream ( ) . asStream ( ) . getInputStream ( ) ;
DeviceContactsInputStream contactsInputStream = new DeviceContactsInputStream ( in ) ;
List < DeviceContact > deviceContacts = contactsInputStream . readAll ( ) ;
for ( DeviceContact deviceContact : deviceContacts ) {
// Check if we have the contact as a friend and that we're not trying to sync our own device
String hexEncodedPublicKey = deviceContact . getNumber ( ) ;
Address address = Address . fromSerialized ( hexEncodedPublicKey ) ;
if ( ! address . isPhone ( ) | | address . toPhoneString ( ) . equals ( TextSecurePreferences . getLocalNumber ( context ) ) ) { continue ; }
/ *
If we ' re not friends with the contact we received or our friend request expired then we should send them a friend request .
Otherwise , if we have received a friend request from them , automatically accept the friend request .
* /
Recipient recipient = Recipient . from ( context , address , false ) ;
long threadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( recipient ) ;
LokiThreadFriendRequestStatus status = DatabaseFactory . getLokiThreadDatabase ( context ) . getFriendRequestStatus ( threadID ) ;
if ( status = = LokiThreadFriendRequestStatus . NONE | | status = = LokiThreadFriendRequestStatus . REQUEST_EXPIRED ) {
// TODO: We should ensure that our mapping has been uploaded to the server before sending out this message
MessageSender . sendBackgroundFriendRequest ( context , hexEncodedPublicKey , "Please accept to enable messages to be synced across devices" ) ;
Log . d ( "Loki" , "Sent friend request to " + hexEncodedPublicKey ) ;
} else if ( status = = LokiThreadFriendRequestStatus . REQUEST_RECEIVED ) {
// Accept the incoming friend request
becomeFriendsWithContactIfNeeded ( hexEncodedPublicKey , false , false ) ;
// Send them an accept message back
MessageSender . sendBackgroundMessage ( context , hexEncodedPublicKey ) ;
Log . d ( "Loki" , "Became friends with " + deviceContact . getNumber ( ) ) ;
}
// TODO: Handle blocked - If user is not blocked then we should do the friend request logic otherwise add them to our block list
// TODO: Handle expiration timer - Update expiration timer?
// TODO: Handle avatar - Download and set avatar?
}
} catch ( Exception e ) {
Log . d ( "Loki" , "Failed to sync contact: " + e + "." ) ;
}
}
private void handleGroupSyncMessage ( @NonNull SignalServiceContent content , @NonNull SignalServiceAttachment groupMessage ) {
if ( groupMessage . isStream ( ) ) {
Log . d ( "Loki" , "Received a group sync message." ) ;
try {
InputStream in = groupMessage . asStream ( ) . getInputStream ( ) ;
DeviceGroupsInputStream groupsInputStream = new DeviceGroupsInputStream ( in ) ;
List < DeviceGroup > groups = groupsInputStream . readAll ( ) ;
for ( DeviceGroup group : groups ) {
SignalServiceGroup serviceGroup = new SignalServiceGroup (
SignalServiceGroup . Type . UPDATE ,
group . getId ( ) ,
SignalServiceGroup . GroupType . SIGNAL ,
group . getName ( ) . orNull ( ) ,
group . getMembers ( ) ,
group . getAvatar ( ) . orNull ( ) ,
group . getAdmins ( )
) ;
SignalServiceDataMessage dataMessage = new SignalServiceDataMessage ( content . getTimestamp ( ) , serviceGroup , null , null ) ;
GroupMessageProcessor . process ( context , content , dataMessage , false ) ;
}
} catch ( Exception e ) {
Log . d ( "Loki" , "Failed to sync group due to error: " + e + "." ) ;
}
}
}
private void handleOpenGroupSyncMessage ( @NonNull List < LokiPublicChat > openGroups ) {
try {
for ( LokiPublicChat openGroup : openGroups ) {
long threadID = GroupManager . getOpenGroupThreadID ( openGroup . getId ( ) , context ) ;
if ( threadID > - 1 ) continue ;
String url = openGroup . getServer ( ) ;
long channel = openGroup . getChannel ( ) ;
OpenGroupUtilities . addGroup ( context , url , channel ) . fail ( e - > {
Log . d ( "Loki" , "Failed to sync open group: " + url + " due to error: " + e + "." ) ;
return Unit . INSTANCE ;
} ) ;
}
} catch ( Exception e ) {
Log . d ( "Loki" , "Failed to sync open groups due to error: " + e + "." ) ;
}
}
private void handleSynchronizeSentMessage ( @NonNull SignalServiceContent content ,
@NonNull SentTranscriptMessage message )
throws StorageFailedException
@ -800,8 +672,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
handleUnknownGroupMessage ( content , message . getMessage ( ) . getGroupInfo ( ) . get ( ) ) ;
}
String ourMasterDevice = TextSecurePreferences . getMasterHexEncodedPublicKey ( context ) ;
boolean isSenderMasterDevice = ourMasterDevice ! = null & & ourMasterDevice . equals ( content . getSender ( ) ) ;
if ( message . getMessage ( ) . getProfileKey ( ) . isPresent ( ) ) {
Recipient recipient = null ;
@ -813,16 +683,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
DatabaseFactory . getRecipientDatabase ( context ) . setProfileSharing ( recipient , true ) ;
}
// Loki - If we received a sync message from our master device then we need to extract the profile picture url
if ( isSenderMasterDevice ) {
handleProfileKey ( content , message . getMessage ( ) ) ;
}
// Loki - Handle profile key update if needed
handleProfileKey ( content , message . getMessage ( ) ) ;
}
// Loki - Update display name from master device
if ( isSenderMasterDevice & & content . senderDisplayName . isPresent ( ) & & content . senderDisplayName . get ( ) . length ( ) > 0 ) {
TextSecurePreferences . setProfileName ( context , content . senderDisplayName . get ( ) ) ;
}
// Loki - Update profile if needed
SessionMetaProtocol . handleProfileUpdateIfNeeded ( content ) ;
if ( threadId ! = null ) {
DatabaseFactory . getThreadDatabase ( context ) . setRead ( threadId , true ) ;
@ -913,17 +779,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Optional < List < LinkPreview > > linkPreviews = getLinkPreviews ( message . getPreviews ( ) , message . getBody ( ) . or ( "" ) ) ;
Optional < Attachment > sticker = getStickerAttachment ( message . getSticker ( ) ) ;
Address sender = masterRecipient . getAddress ( ) ;
Address masterAddress = masterRecipient . getAddress ( ) ;
// If message is from group then we need to map it to get the sender of the message
if ( message . isGroupMessage ( ) ) {
sender = getMasterRecipient ( content . getSender ( ) ) . getAddress ( ) ;
masterAddress = getMasterRecipient ( content . getSender ( ) ) . getAddress ( ) ;
}
// Ignore messages from ourselves
if ( sender . serialize ( ) . equalsIgnoreCase ( TextSecurePreferences . getLocalNumber ( context ) ) ) { return ; }
IncomingMediaMessage mediaMessage = new IncomingMediaMessage ( sender , message . getTimestamp ( ) , - 1 ,
IncomingMediaMessage mediaMessage = new IncomingMediaMessage ( masterAddress , message . getTimestamp ( ) , - 1 ,
message . getExpiresInSeconds ( ) * 1000L , false , content . isNeedsReceipt ( ) , message . getBody ( ) , message . getGroupInfo ( ) , message . getAttachments ( ) ,
quote , sharedContacts , linkPreviews , sticker ) ;
@ -967,19 +829,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
MessageNotifier . updateNotification ( context , insertResult . get ( ) . getThreadId ( ) ) ;
}
// Loki - Run database updates in the background, we should look into fixing this in the future
AsyncTask . execute ( ( ) - > {
// Loki - Store message server ID
updateGroupChatMessageServerID ( messageServerIDOrNull , insertResult ) ;
// Loki - Store message server ID if needed
updateGroupChatMessageServerID ( messageServerIDOrNull , insertResult ) ;
// Loki - Update mapping of message to original thread ID
if ( insertResult . isPresent ( ) ) {
ThreadDatabase threadDatabase = DatabaseFactory . getThreadDatabase ( context ) ;
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory . getLokiMessageDatabase ( context ) ;
long originalThreadId = threadDatabase . getThreadIdFor ( originalRecipient ) ;
lokiMessageDatabase . setOriginalThreadID ( insertResult . get ( ) . getMessageId ( ) , originalThreadId ) ;
}
} ) ;
// Loki - Update mapping of message ID to original thread ID
if ( insertResult . isPresent ( ) ) {
ThreadDatabase threadDatabase = DatabaseFactory . getThreadDatabase ( context ) ;
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory . getLokiMessageDatabase ( context ) ;
long originalThreadId = threadDatabase . getThreadIdFor ( originalRecipient ) ;
lokiMessageDatabase . setOriginalThreadID ( insertResult . get ( ) . getMessageId ( ) , originalThreadId ) ;
}
}
private long handleSynchronizeSentExpirationUpdate ( @NonNull SentTranscriptMessage message ) throws MmsException {
@ -1108,14 +967,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Address sender = masterRecipient . getAddress ( ) ;
// If message is from group then we need to map it to get the sender of the message
if ( message . isGroupMessage ( ) ) {
sender = getMasterRecipient ( content . getSender ( ) ) . getAddress ( ) ;
}
// Ignore messages from ourselves
if ( sender . serialize ( ) . equalsIgnoreCase ( TextSecurePreferences . getLocalNumber ( context ) ) ) { return ; }
IncomingTextMessage tm = new IncomingTextMessage ( sender ,
content . getSenderDevice ( ) ,
message . getTimestamp ( ) , body ,
@ -1141,249 +996,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
MessageNotifier . updateNotification ( context , threadId ) ;
}
// Loki - Run database updates in background, we should look into fixing this in the future
AsyncTask . execute ( ( ) - > {
if ( insertResult . isPresent ( ) ) {
InsertResult result = insertResult . get ( ) ;
// Loki - Cache the user hex encoded public key (for mentions)
MentionManagerUtilities . INSTANCE . populateUserHexEncodedPublicKeyCacheIfNeeded ( result . getThreadId ( ) , context ) ;
MentionsManager . INSTANCE . cache ( textMessage . getSender ( ) . serialize ( ) , result . getThreadId ( ) ) ;
// Loki - Store message server ID
updateGroupChatMessageServerID ( messageServerIDOrNull , insertResult ) ;
// Loki - Update mapping of message to original thread ID
if ( result . getMessageId ( ) > - 1 ) {
ThreadDatabase threadDatabase = DatabaseFactory . getThreadDatabase ( context ) ;
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory . getLokiMessageDatabase ( context ) ;
long originalThreadId = threadDatabase . getThreadIdFor ( originalRecipient ) ;
lokiMessageDatabase . setOriginalThreadID ( result . getMessageId ( ) , originalThreadId ) ;
}
}
} ) ;
}
}
private boolean isValidDeviceLinkMessage ( @NonNull DeviceLink authorisation ) {
boolean isSecondaryDevice = TextSecurePreferences . getMasterHexEncodedPublicKey ( context ) ! = null ;
String userHexEncodedPublicKey = TextSecurePreferences . getLocalNumber ( context ) ;
boolean isRequest = ( authorisation . getType ( ) = = DeviceLink . Type . REQUEST ) ;
if ( authorisation . getRequestSignature ( ) = = null ) {
Log . d ( "Loki" , "Ignoring pairing request message without a request signature." ) ;
return false ;
} else if ( isRequest & & isSecondaryDevice ) {
Log . d ( "Loki" , "Ignoring unexpected pairing request message (the device is already paired as a secondary device)." ) ;
return false ;
} else if ( isRequest & & ! authorisation . getMasterHexEncodedPublicKey ( ) . equals ( userHexEncodedPublicKey ) ) {
Log . d ( "Loki" , "Ignoring pairing request message addressed to another user." ) ;
return false ;
} else if ( isRequest & & authorisation . getSlaveHexEncodedPublicKey ( ) . equals ( userHexEncodedPublicKey ) ) {
Log . d ( "Loki" , "Ignoring pairing request message from self." ) ;
return false ;
}
return authorisation . verify ( ) ;
}
private void handleDeviceLinkMessage ( @NonNull DeviceLink deviceLink , @NonNull SignalServiceContent content ) {
String userHexEncodedPublicKey = TextSecurePreferences . getLocalNumber ( context ) ;
if ( deviceLink . getType ( ) = = DeviceLink . Type . REQUEST ) {
handleDeviceLinkRequestMessage ( deviceLink , content ) ;
} else if ( deviceLink . getSlaveHexEncodedPublicKey ( ) . equals ( userHexEncodedPublicKey ) ) {
handleDeviceLinkAuthorizedMessage ( deviceLink , content ) ;
}
}
private void handleDeviceLinkRequestMessage ( @NonNull DeviceLink deviceLink , @NonNull SignalServiceContent content ) {
DeviceLinkingSession linkingSession = DeviceLinkingSession . Companion . getShared ( ) ;
if ( ! linkingSession . isListeningForLinkingRequests ( ) ) {
new Broadcaster ( context ) . broadcast ( "unexpectedDeviceLinkRequestReceived" ) ;
return ;
}
boolean isValid = isValidDeviceLinkMessage ( deviceLink ) ;
if ( ! isValid ) { return ; }
storePreKeyBundleIfNeeded ( content ) ;
linkingSession . processLinkingRequest ( deviceLink ) ;
}
private void handleDeviceLinkAuthorizedMessage ( @NonNull DeviceLink deviceLink , @NonNull SignalServiceContent content ) {
// Check preconditions
boolean hasExistingDeviceLink = TextSecurePreferences . getMasterHexEncodedPublicKey ( context ) ! = null ;
if ( hasExistingDeviceLink ) {
Log . d ( "Loki" , "Ignoring unexpected device link message (the device is already linked as a slave device)." ) ;
return ;
}
boolean isValid = isValidDeviceLinkMessage ( deviceLink ) ;
if ( ! isValid ) {
Log . d ( "Loki" , "Ignoring invalid device link message." ) ;
return ;
}
if ( ! DeviceLinkingSession . Companion . getShared ( ) . isListeningForLinkingRequests ( ) ) {
Log . d ( "Loki" , "Ignoring device link message." ) ;
return ;
}
if ( deviceLink . getType ( ) ! = DeviceLink . Type . AUTHORIZATION ) { return ; }
Log . d ( "Loki" , "Received device link authorized message from: " + deviceLink . getMasterHexEncodedPublicKey ( ) + "." ) ;
// Save pre key bundle if we somehow got one
storePreKeyBundleIfNeeded ( content ) ;
// Process
DeviceLinkingSession . Companion . getShared ( ) . processLinkingAuthorization ( deviceLink ) ;
// Store the master device's ID
String userHexEncodedPublicKey = TextSecurePreferences . getLocalNumber ( context ) ;
DatabaseFactory . getLokiAPIDatabase ( context ) . clearDeviceLinks ( userHexEncodedPublicKey ) ;
DatabaseFactory . getLokiAPIDatabase ( context ) . addDeviceLink ( deviceLink ) ;
TextSecurePreferences . setMasterHexEncodedPublicKey ( context , deviceLink . getMasterHexEncodedPublicKey ( ) ) ;
TextSecurePreferences . setMultiDevice ( context , true ) ;
// Send a background message to the master device
MessageSender . sendBackgroundMessage ( context , deviceLink . getMasterHexEncodedPublicKey ( ) ) ;
/ *
Update device link on the file server .
We put this here because after receiving the authorisation message , we will also receive all sync messages .
If these sync messages are contact syncs then we need to send them friend requests so that we can establish multi - device communication .
If our device mapping is not stored on the server before the other party receives our message , they will think that they got a friend request from a non - multi - device user .
* /
try {
PromiseUtil . timeout ( LokiFileServerAPI . shared . addDeviceLink ( deviceLink ) , 8000 ) . get ( ) ;
} catch ( Exception e ) {
Log . w ( "Loki" , "Failed to upload device links to the file server! " + e ) ;
}
// Update display name if needed
if ( content . senderDisplayName . isPresent ( ) & & content . senderDisplayName . get ( ) . length ( ) > 0 ) {
TextSecurePreferences . setProfileName ( context , content . senderDisplayName . get ( ) ) ;
}
// Update profile picture if needed
if ( content . getDataMessage ( ) . isPresent ( ) ) {
handleProfileKey ( content , content . getDataMessage ( ) . get ( ) ) ;
}
// Handle contact sync if needed
if ( content . getSyncMessage ( ) . isPresent ( ) & & content . getSyncMessage ( ) . get ( ) . getContacts ( ) . isPresent ( ) ) {
handleContactSyncMessage ( content . getSyncMessage ( ) . get ( ) . getContacts ( ) . get ( ) ) ;
}
}
if ( insertResult . isPresent ( ) ) {
InsertResult result = insertResult . get ( ) ;
private void setDisplayName ( String hexEncodedPublicKey , String profileName ) {
String displayName = profileName + " (..." + hexEncodedPublicKey . substring ( hexEncodedPublicKey . length ( ) - 8 ) + ")" ;
DatabaseFactory . getLokiUserDatabase ( context ) . setDisplayName ( hexEncodedPublicKey , displayName ) ;
}
// Loki - Cache the user hex encoded public key (for mentions)
MentionManagerUtilities . INSTANCE . populateUserHexEncodedPublicKeyCacheIfNeeded ( result . getThreadId ( ) , context ) ;
MentionsManager . shared . cache ( textMessage . getSender ( ) . serialize ( ) , result . getThreadId ( ) ) ;
private void updateGroupChatMessageServerID ( Optional < Long > messageServerIDOrNull , Optional < InsertResult > insertResult ) {
if ( ! insertResult . isPresent ( ) | | ! messageServerIDOrNull . isPresent ( ) ) { return ; }
long messageID = insertResult . get ( ) . getMessageId ( ) ;
long messageServerID = messageServerIDOrNull . get ( ) ;
DatabaseFactory . getLokiMessageDatabase ( context ) . setServerID ( messageID , messageServerID ) ;
}
private void storePreKeyBundleIfNeeded ( @NonNull SignalServiceContent content ) {
Recipient sender = Recipient . from ( context , Address . fromSerialized ( content . getSender ( ) ) , false ) ;
if ( sender . isGroupRecipient ( ) | | ! content . lokiServiceMessage . isPresent ( ) ) { return ; }
LokiServiceMessage lokiMessage = content . lokiServiceMessage . get ( ) ;
if ( lokiMessage . getPreKeyBundleMessage ( ) = = null ) { return ; }
int registrationID = TextSecurePreferences . getLocalRegistrationId ( context ) ;
LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory . getLokiPreKeyBundleDatabase ( context ) ;
if ( registrationID < = 0 ) { return ; }
Log . d ( "Loki" , "Received a pre key bundle from: " + content . getSender ( ) + "." ) ;
PreKeyBundle preKeyBundle = lokiMessage . getPreKeyBundleMessage ( ) . getPreKeyBundle ( registrationID ) ;
lokiPreKeyBundleDatabase . setPreKeyBundle ( content . getSender ( ) , preKeyBundle ) ;
// Loki - Store message server ID
updateGroupChatMessageServerID ( messageServerIDOrNull , insertResult ) ;
}
private void handleSessionRequestIfNeeded ( @NonNull SignalServiceContent content ) {
if ( ! content . isFriendRequest ( ) | | ! isSessionRequest ( content ) ) { return ; }
// Check if the session request came from a member in one of our groups or one of our friends
LokiDeviceLinkUtilities . INSTANCE . getMasterHexEncodedPublicKey ( content . getSender ( ) ) . success ( masterHexEncodedPublicKey - > {
String sender = masterHexEncodedPublicKey ! = null ? masterHexEncodedPublicKey : content . getSender ( ) ;
long threadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( Recipient . from ( context , Address . fromSerialized ( sender ) , false ) ) ;
LokiThreadFriendRequestStatus threadFriendRequestStatus = DatabaseFactory . getLokiThreadDatabase ( context ) . getFriendRequestStatus ( threadID ) ;
boolean isOurFriend = threadFriendRequestStatus = = LokiThreadFriendRequestStatus . FRIENDS ;
boolean isInOneOfOurGroups = DatabaseFactory . getGroupDatabase ( context ) . isClosedGroupMember ( sender ) ;
boolean shouldAcceptSessionRequest = isOurFriend | | isInOneOfOurGroups ;
if ( shouldAcceptSessionRequest ) {
MessageSender . sendBackgroundMessage ( context , content . getSender ( ) ) ; // Send a background message to acknowledge
}
return Unit . INSTANCE ;
} ) ;
}
private void becomeFriendsWithContactIfNeeded ( String hexEncodedPublicKey , boolean requiresContactSync , boolean canSkip ) {
// Ignore friend requests to group recipients
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory . getLokiThreadDatabase ( context ) ;
Recipient contactID = Recipient . from ( context , Address . fromSerialized ( hexEncodedPublicKey ) , false ) ;
if ( contactID . isGroupRecipient ( ) ) return ;
// Ignore friend requests to recipients we're already friends with
long threadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( contactID ) ;
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase . getFriendRequestStatus ( threadID ) ;
if ( threadFriendRequestStatus = = LokiThreadFriendRequestStatus . FRIENDS ) { return ; }
// We shouldn't be able to skip from NONE to FRIENDS under normal circumstances.
// Multi-device is the one exception to this rule because we want to automatically become friends with slave devices.
if ( ! canSkip & & threadFriendRequestStatus = = LokiThreadFriendRequestStatus . NONE ) { return ; }
// If the thread's friend request status is not `FRIENDS` or `NONE`, but we're receiving a message,
// it must be a friend request accepted message. Declining a friend request doesn't send a message.
lokiThreadDatabase . setFriendRequestStatus ( threadID , LokiThreadFriendRequestStatus . FRIENDS ) ;
// Send out a contact sync message if needed
if ( requiresContactSync ) {
MessageSender . syncContact ( context , contactID . getAddress ( ) ) ;
}
// Enable profile sharing with the recipient
DatabaseFactory . getRecipientDatabase ( context ) . setProfileSharing ( contactID , true ) ;
// Update the last message if needed
LokiDeviceLinkUtilities . INSTANCE . getMasterHexEncodedPublicKey ( hexEncodedPublicKey ) . success ( masterHexEncodedPublicKey - > {
Util . runOnMain ( ( ) - > {
long masterThreadID = ( masterHexEncodedPublicKey = = null ) ? threadID : DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( Recipient . from ( context , Address . fromSerialized ( masterHexEncodedPublicKey ) , false ) ) ;
FriendRequestHandler . updateLastFriendRequestMessage ( context , masterThreadID , LokiMessageFriendRequestStatus . REQUEST_ACCEPTED ) ;
} ) ;
return Unit . INSTANCE ;
} ) ;
}
private void updateFriendRequestStatusIfNeeded ( @NonNull SignalServiceContent content , @NonNull SignalServiceDataMessage message ) {
if ( ! content . isFriendRequest ( ) | | message . isGroupMessage ( ) | | message . isSessionRequest ( ) ) { return ; }
Promise < Boolean , Exception > promise = PromiseUtil . timeout ( MultiDeviceUtilities . shouldAutomaticallyBecomeFriendsWithDevice ( content . getSender ( ) , context ) , 8000 ) ;
boolean shouldBecomeFriends = PromiseUtil . get ( promise , false ) ;
if ( shouldBecomeFriends ) {
// Become friends AND update the message they sent
becomeFriendsWithContactIfNeeded ( content . getSender ( ) , true , true ) ;
// Send them an accept message back
MessageSender . sendBackgroundMessage ( context , content . getSender ( ) ) ;
} else {
// Do regular friend request logic checks
Recipient originalRecipient = getRecipientForMessage ( content , message ) ;
Recipient masterRecipient = getMasterRecipientForMessage ( content , message ) ;
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory . getLokiThreadDatabase ( context ) ;
// Loki - Friend requests only work in direct chats
if ( ! originalRecipient . getAddress ( ) . isPhone ( ) ) { return ; }
long threadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdIfExistsFor ( originalRecipient ) ;
long primaryDeviceThreadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdIfExistsFor ( masterRecipient ) ;
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase . getFriendRequestStatus ( threadID ) ;
if ( threadFriendRequestStatus = = LokiThreadFriendRequestStatus . REQUEST_SENT ) {
// This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his
// mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request
// and send a friend request accepted message back to Bob. We don't check that sending the
// friend request accepted message succeeded. Even if it doesn't, the thread's current friend
// request status will be set to `FRIENDS` for Alice making it possible
// for Alice to send messages to Bob. When Bob receives a message, his thread's friend request status
// will then be set to `FRIENDS`. If we do check for a successful send
// before updating Alice's thread's friend request status to `FRIENDS`,
// we can end up in a deadlock where both users' threads' friend request statuses are
// `REQUEST_SENT`.
lokiThreadDatabase . setFriendRequestStatus ( threadID , LokiThreadFriendRequestStatus . FRIENDS ) ;
// Since messages are forwarded to the primary device thread, we need to update it there
FriendRequestHandler . updateLastFriendRequestMessage ( context , primaryDeviceThreadID , LokiMessageFriendRequestStatus . REQUEST_ACCEPTED ) ;
// Accept the friend request
MessageSender . sendBackgroundMessage ( context , content . getSender ( ) ) ;
// Send contact sync message
MessageSender . syncContact ( context , originalRecipient . getAddress ( ) ) ;
} else if ( threadFriendRequestStatus ! = LokiThreadFriendRequestStatus . FRIENDS ) {
// Checking that the sender of the message isn't already a friend is necessary because otherwise
// the following situation can occur: Alice and Bob are friends. Bob loses his database and his
// friend request status is reset to `NONE`. Bob now sends Alice a friend
// request. Alice's thread's friend request status is reset to
// `REQUEST_RECEIVED`.
lokiThreadDatabase . setFriendRequestStatus ( threadID , LokiThreadFriendRequestStatus . REQUEST_RECEIVED ) ;
// Since messages are forwarded to the primary device thread, we need to update it there
FriendRequestHandler . receivedIncomingFriendRequestMessage ( context , primaryDeviceThreadID ) ;
// Loki - Update mapping of message to original thread ID
if ( result . getMessageId ( ) > - 1 ) {
ThreadDatabase threadDatabase = DatabaseFactory . getThreadDatabase ( context ) ;
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory . getLokiMessageDatabase ( context ) ;
long originalThreadId = threadDatabase . getThreadIdFor ( originalRecipient ) ;
lokiMessageDatabase . setOriginalThreadID ( result . getMessageId ( ) , originalThreadId ) ;
}
}
}
}
@ -1464,21 +1093,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
private SmsMessageRecord getLastMessage ( String sender ) {
try {
SmsDatabase smsDatabase = DatabaseFactory . getSmsDatabase ( context ) ;
Recipient recipient = Recipient . from ( context , Address . fromSerialized ( sender ) , false ) ;
long threadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdIfExistsFor ( recipient ) ;
if ( threadID < 0 ) { return null ; }
int messageCount = smsDatabase . getMessageCountForThread ( threadID ) ;
if ( messageCount < = 0 ) { return null ; }
long lastMessageID = smsDatabase . getIDForMessageAtIndex ( threadID , messageCount - 1 ) ;
return smsDatabase . getMessage ( lastMessageID ) ;
} catch ( Exception e ) {
return null ;
}
}
private void handleCorruptMessage ( @NonNull String sender , int senderDevice , long timestamp ,
@NonNull Optional < Long > smsMessageId )
{
@ -1521,14 +1135,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
triggerSessionRestorePrompt ( sender ) ;
}
private void triggerSessionRestorePrompt ( @NonNull String sender ) {
Recipient primaryRecipient = getMasterRecipient ( sender ) ;
if ( ! primaryRecipient . isGroupRecipient ( ) ) {
long threadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( primaryRecipient ) ;
DatabaseFactory . getLokiThreadDatabase ( context ) . addSessionRestoreDevice ( threadID , sender ) ;
}
}
private void handleLegacyMessage ( @NonNull String sender , int senderDevice , long timestamp ,
@NonNull Optional < Long > smsMessageId )
{
@ -1580,10 +1186,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
String url = content . senderProfilePictureURL . or ( "" ) ;
ApplicationContext . getInstance ( context ) . getJobManager ( ) . add ( new RetrieveProfileAvatarJob ( recipient , url ) ) ;
// Loki - If the recipient is our master device then we need to go and update our avatar mappings on the public chats
if ( recipient . isUserMasterDevice ( ) ) {
ApplicationContext . getInstance ( context ) . updatePublicChatProfilePictureIfNeeded ( ) ;
}
SessionMetaProtocol . handleProfileKeyUpdateIfNeeded ( content , message ) ;
}
}
@ -1647,7 +1250,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
long threadId ;
if ( typingMessage . getGroupId ( ) . isPresent ( ) ) {
// Typing messages should only apply to signal groups, thus we use `getEncodedId`
// Typing messages should only apply to closed groups, thus we use `getEncodedId`
Address groupAddress = Address . fromSerialized ( GroupUtil . getEncodedId ( typingMessage . getGroupId ( ) . get ( ) , false ) ) ;
Recipient groupRecipient = Recipient . from ( context , groupAddress , false ) ;
@ -1814,45 +1417,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
private Recipient getRecipientForMessage ( SignalServiceContent content , SignalServiceDataMessage message ) {
if ( message . isGroupMessage ( ) ) {
return Recipient . from ( context , Address . fromSerialized ( GroupUtil . getEncodedId ( message . getGroupInfo ( ) . get ( ) ) ) , false ) ;
} else {
return Recipient . from ( context , Address . fromSerialized ( content . getSender ( ) ) , false ) ;
}
}
private Recipient getMasterRecipientForMessage ( SignalServiceContent content , SignalServiceDataMessage message ) {
if ( message . isGroupMessage ( ) ) {
return getRecipientForMessage ( content , message ) ;
} else {
return getMasterRecipient ( content . getSender ( ) ) ;
}
}
/ * *
* Get the master device recipient of the provided device .
*
* If the device doesn ' t have a master device this will return the same device .
* If the device is our master device then it will return our current device .
* Otherwise it will return the master device .
* /
private Recipient getMasterRecipient ( String hexEncodedPublicKey ) {
try {
String masterHexEncodedPublicKey = PromiseUtil . timeout ( LokiDeviceLinkUtilities . INSTANCE . getMasterHexEncodedPublicKey ( hexEncodedPublicKey ) , 5000 ) . get ( ) ;
String targetHexEncodedPublicKey = ( masterHexEncodedPublicKey ! = null ) ? masterHexEncodedPublicKey : hexEncodedPublicKey ;
// If the public key matches our master device then we need to forward the message to ourselves (note to self)
String ourMasterHexEncodedPublicKey = TextSecurePreferences . getMasterHexEncodedPublicKey ( context ) ;
if ( ourMasterHexEncodedPublicKey ! = null & & ourMasterHexEncodedPublicKey . equals ( targetHexEncodedPublicKey ) ) {
targetHexEncodedPublicKey = TextSecurePreferences . getLocalNumber ( context ) ;
}
return Recipient . from ( context , Address . fromSerialized ( targetHexEncodedPublicKey ) , false ) ;
} catch ( Exception e ) {
Log . d ( "Loki" , "Failed to get master device for: " + hexEncodedPublicKey + ". " + e . getMessage ( ) ) ;
return Recipient . from ( context , Address . fromSerialized ( hexEncodedPublicKey ) , false ) ;
}
}
private void notifyTypingStoppedFromIncomingMessage ( @NonNull Recipient conversationRecipient , @NonNull String sender , int device ) {
Recipient author = Recipient . from ( context , Address . fromSerialized ( sender ) , false ) ;
long threadId = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( conversationRecipient ) ;
@ -1895,53 +1459,20 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
boolean isGroupActive = groupId . isPresent ( ) & & groupDatabase . isActive ( groupId . get ( ) ) ;
boolean isLeaveMessage = message . getGroupInfo ( ) . isPresent ( ) & & message . getGroupInfo ( ) . get ( ) . getType ( ) = = SignalServiceGroup . Type . QUIT ;
boolean isClosedGroup = conversation . getAddress ( ) . isClosedGroup ( ) ;
boolean isGroupMember = true ;
// Only allow messages from group members
if ( isClosedGroup ) {
String senderHexEncodedPublicKey = content . getSender ( ) ;
try {
String masterHexEncodedPublicKey = PromiseUtil . timeout ( LokiDeviceLinkUtilities . INSTANCE . getMasterHexEncodedPublicKey ( content . getSender ( ) ) , 5000 ) . get ( ) ;
if ( masterHexEncodedPublicKey ! = null ) {
senderHexEncodedPublicKey = masterHexEncodedPublicKey ;
}
} catch ( Exception e ) {
e . printStackTrace ( ) ;
}
Recipient senderMasterAddress = Recipient . from ( context , Address . fromSerialized ( senderHexEncodedPublicKey ) , false ) ;
isGroupMember = groupId . isPresent ( ) & & groupDatabase . getGroupMembers ( groupId . get ( ) , true ) . contains ( senderMasterAddress ) ;
}
return ( isContentMessage & & ! isGroupActive ) | | ( sender . isBlocked ( ) & & ! isLeaveMessage ) | | ( isContentMessage & & ! isGroupMember ) ;
boolean shouldIgnoreContentMessage = ClosedGroupsProtocol . shouldIgnoreContentMessage ( context , conversation , groupId . orNull ( ) , content ) ;
return ( isContentMessage & & ! isGroupActive ) | | ( sender . isBlocked ( ) & & ! isLeaveMessage ) | | ( isContentMessage & & ! shouldIgnoreContentMessage ) ;
} else {
return sender . isBlocked ( ) ;
}
} else if ( content . getCallMessage ( ) . isPresent ( ) | | content . getTypingMessage ( ) . isPresent ( ) ) {
return sender . isBlocked ( ) ;
} else if ( content . getSyncMessage ( ) . isPresent ( ) ) {
try {
// We should ignore a sync message if the sender is not one of our devices
boolean isOurDevice = PromiseUtil . timeout ( MultiDeviceUtilities . isOneOfOurDevices ( context , sender . getAddress ( ) ) , 5000 ) . get ( ) ;
if ( ! isOurDevice ) {
Log . w ( TAG , "Got a sync message from a device that is not ours!." ) ;
}
return ! isOurDevice ;
} catch ( Exception e ) {
return true ;
}
return SyncMessagesProtocol . shouldIgnoreSyncMessage ( context , sender ) ;
}
return false ;
}
private boolean isSessionRequest ( SignalServiceContent content ) {
return content . getDataMessage ( ) . isPresent ( ) & & content . getDataMessage ( ) . get ( ) . isSessionRequest ( ) ;
}
private boolean isGroupChatMessage ( SignalServiceContent content ) {
return content . getDataMessage ( ) . isPresent ( ) & & content . getDataMessage ( ) . get ( ) . isGroupMessage ( ) ;
}