2020-05-11 16:19:26 +10:00
package org.thoughtcrime.securesms.loki.api
2019-08-05 16:27:37 +10:00
import android.content.Context
2019-08-06 16:18:24 +10:00
import android.os.Handler
2019-08-05 16:27:37 +10:00
import android.util.Log
2019-11-18 11:55:16 +11:00
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.then
2019-11-28 12:49:33 +11:00
import org.thoughtcrime.securesms.ApplicationContext
2019-08-23 13:21:48 +10:00
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
2019-11-28 12:49:33 +11:00
import org.thoughtcrime.securesms.database.Address
2019-08-08 17:01:57 +10:00
import org.thoughtcrime.securesms.database.DatabaseFactory
2019-08-05 16:27:37 +10:00
import org.thoughtcrime.securesms.jobs.PushDecryptJob
2019-11-28 12:49:33 +11:00
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.recipients.Recipient
2019-08-08 17:01:57 +10:00
import org.thoughtcrime.securesms.util.TextSecurePreferences
2019-08-05 16:27:37 +10:00
import org.whispersystems.libsignal.util.guava.Optional
2019-10-21 15:26:57 +11:00
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.messages.SignalServiceGroup
2019-11-15 11:37:36 +11:00
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage
2019-08-05 16:27:37 +10:00
import org.whispersystems.signalservice.api.push.SignalServiceAddress
2020-04-09 13:38:15 +10:00
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
2020-05-07 17:59:41 +10:00
import org.whispersystems.signalservice.loki.protocol.multidevice.LokiDeviceLinkUtilities
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChatAPI
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChatMessage
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
2020-03-27 10:35:44 +11:00
import org.whispersystems.signalservice.loki.utilities.successBackground
2019-11-28 12:49:33 +11:00
import java.security.MessageDigest
2019-11-15 11:37:36 +11:00
import java.util.*
2019-08-05 16:27:37 +10:00
2019-10-15 13:39:17 +11:00
class LokiPublicChatPoller ( private val context : Context , private val group : LokiPublicChat ) {
2019-08-06 16:18:24 +10:00
private val handler = Handler ( )
private var hasStarted = false
2019-08-05 16:27:37 +10:00
2019-09-04 11:11:48 +10:00
// region Convenience
private val userHexEncodedPublicKey = TextSecurePreferences . getLocalNumber ( context )
2019-11-18 11:08:23 +11:00
private var displayNameUpdatees = setOf < String > ( )
2019-09-04 11:11:48 +10:00
2019-10-15 13:39:17 +11:00
private val api : LokiPublicChatAPI
2019-08-26 13:03:38 +10:00
get ( ) = {
2019-10-07 10:06:19 +11:00
val userPrivateKey = IdentityKeyUtil . getIdentityKeyPair ( context ) . privateKey . serialize ( )
2019-08-26 13:03:38 +10:00
val lokiAPIDatabase = DatabaseFactory . getLokiAPIDatabase ( context )
val lokiUserDatabase = DatabaseFactory . getLokiUserDatabase ( context )
2019-10-15 13:39:17 +11:00
LokiPublicChatAPI ( userHexEncodedPublicKey , userPrivateKey , lokiAPIDatabase , lokiUserDatabase )
2019-08-26 13:03:38 +10:00
} ( )
2019-09-04 11:11:48 +10:00
// endregion
2019-08-26 13:03:38 +10:00
2019-09-04 11:11:48 +10:00
// region Tasks
2019-08-26 13:03:38 +10:00
private val pollForNewMessagesTask = object : Runnable {
override fun run ( ) {
pollForNewMessages ( )
handler . postDelayed ( this , pollForNewMessagesInterval )
}
}
private val pollForDeletedMessagesTask = object : Runnable {
2019-08-06 16:18:24 +10:00
override fun run ( ) {
2019-08-26 13:03:38 +10:00
pollForDeletedMessages ( )
handler . postDelayed ( this , pollForDeletedMessagesInterval )
2019-08-06 16:18:24 +10:00
}
}
2019-09-12 16:42:52 +10:00
private val pollForModeratorsTask = object : Runnable {
2019-09-02 16:42:08 +10:00
override fun run ( ) {
2019-09-12 16:42:52 +10:00
pollForModerators ( )
handler . postDelayed ( this , pollForModeratorsInterval )
2019-09-02 16:42:08 +10:00
}
}
2019-11-18 11:08:23 +11:00
private val pollForDisplayNamesTask = object : Runnable {
2020-03-24 11:31:01 +11:00
2019-11-18 11:08:23 +11:00
override fun run ( ) {
pollForDisplayNames ( )
handler . postDelayed ( this , pollForDisplayNamesInterval )
}
}
2019-09-04 11:11:48 +10:00
// endregion
2019-09-02 16:42:08 +10:00
2019-09-04 11:11:48 +10:00
// region Settings
2019-08-06 16:18:24 +10:00
companion object {
2019-08-26 13:03:38 +10:00
private val pollForNewMessagesInterval : Long = 4 * 1000
2019-08-30 17:08:46 +10:00
private val pollForDeletedMessagesInterval : Long = 20 * 1000
2019-09-12 16:42:52 +10:00
private val pollForModeratorsInterval : Long = 10 * 60 * 1000
2019-11-18 13:12:57 +11:00
private val pollForDisplayNamesInterval : Long = 60 * 1000
2019-08-06 16:18:24 +10:00
}
2019-09-04 11:11:48 +10:00
// endregion
2019-08-06 16:18:24 +10:00
2019-09-04 11:11:48 +10:00
// region Lifecycle
2019-08-06 16:18:24 +10:00
fun startIfNeeded ( ) {
if ( hasStarted ) return
2019-08-26 13:03:38 +10:00
pollForNewMessagesTask . run ( )
pollForDeletedMessagesTask . run ( )
2019-09-12 16:42:52 +10:00
pollForModeratorsTask . run ( )
2019-11-18 11:08:23 +11:00
pollForDisplayNamesTask . run ( )
2019-08-06 16:18:24 +10:00
hasStarted = true
}
fun stop ( ) {
2019-08-26 13:03:38 +10:00
handler . removeCallbacks ( pollForNewMessagesTask )
handler . removeCallbacks ( pollForDeletedMessagesTask )
2019-09-12 16:42:52 +10:00
handler . removeCallbacks ( pollForModeratorsTask )
2019-11-18 11:08:23 +11:00
handler . removeCallbacks ( pollForDisplayNamesTask )
2019-08-21 11:04:05 +10:00
hasStarted = false
2019-08-06 16:18:24 +10:00
}
2019-09-04 11:11:48 +10:00
// endregion
2019-08-06 16:18:24 +10:00
2019-09-04 11:11:48 +10:00
// region Polling
2019-11-15 11:37:36 +11:00
private fun getDataMessage ( message : LokiPublicChatMessage ) : SignalServiceDataMessage {
val id = group . id . toByteArray ( )
2019-12-13 10:25:53 +11:00
val serviceGroup = SignalServiceGroup ( SignalServiceGroup . Type . UPDATE , id , SignalServiceGroup . GroupType . PUBLIC _CHAT , null , null , null , null )
2019-11-15 11:37:36 +11:00
val quote = if ( message . quote != null ) {
SignalServiceDataMessage . Quote ( message . quote !! . quotedMessageTimestamp , SignalServiceAddress ( message . quote !! . quoteeHexEncodedPublicKey ) , message . quote !! . quotedMessageBody , listOf ( ) )
} else {
null
}
val attachments = message . attachments . mapNotNull { attachment ->
if ( attachment . kind != LokiPublicChatMessage . Attachment . Kind . Attachment ) { return @mapNotNull null }
SignalServiceAttachmentPointer (
2020-02-01 07:58:32 +11:00
attachment . serverID ,
attachment . contentType ,
ByteArray ( 0 ) ,
Optional . of ( attachment . size ) ,
Optional . absent ( ) ,
attachment . width , attachment . height ,
Optional . absent ( ) ,
Optional . of ( attachment . fileName ) ,
false ,
Optional . fromNullable ( attachment . caption ) ,
attachment . url )
2019-11-15 11:37:36 +11:00
}
val linkPreview = message . attachments . firstOrNull { it . kind == LokiPublicChatMessage . Attachment . Kind . LinkPreview }
val signalLinkPreviews = mutableListOf < SignalServiceDataMessage . Preview > ( )
if ( linkPreview != null ) {
val attachment = SignalServiceAttachmentPointer (
2020-02-01 07:58:32 +11:00
linkPreview . serverID ,
linkPreview . contentType ,
ByteArray ( 0 ) ,
Optional . of ( linkPreview . size ) ,
Optional . absent ( ) ,
linkPreview . width , linkPreview . height ,
Optional . absent ( ) ,
Optional . of ( linkPreview . fileName ) ,
false ,
Optional . fromNullable ( linkPreview . caption ) ,
linkPreview . url )
2019-11-15 11:37:36 +11:00
signalLinkPreviews . add ( SignalServiceDataMessage . Preview ( linkPreview . linkPreviewURL !! , linkPreview . linkPreviewTitle !! , Optional . of ( attachment ) ) )
}
val body = if ( message . body == message . timestamp . toString ( ) ) " " else message . body // Workaround for the fact that the back-end doesn't accept messages without a body
return SignalServiceDataMessage ( message . timestamp , serviceGroup , attachments , body , false , 0 , false , null , false , quote , null , signalLinkPreviews , null )
}
2020-01-22 11:57:16 +11:00
fun pollForNewMessages ( ) {
2019-11-15 11:37:36 +11:00
fun processIncomingMessage ( message : LokiPublicChatMessage ) {
2020-02-12 16:42:33 +11:00
// If the sender of the current message is not a slave device, set the display name in the database
val masterHexEncodedPublicKey = LokiDeviceLinkUtilities . getMasterHexEncodedPublicKey ( message . hexEncodedPublicKey ) . get ( )
if ( masterHexEncodedPublicKey == null ) {
2019-11-18 11:08:23 +11:00
val senderDisplayName = " ${message.displayName} (... ${message.hexEncodedPublicKey.takeLast(8)} ) "
DatabaseFactory . getLokiUserDatabase ( context ) . setServerDisplayName ( group . id , message . hexEncodedPublicKey , senderDisplayName )
}
2020-02-12 16:42:33 +11:00
val senderHexEncodedPublicKey = masterHexEncodedPublicKey ?: message . hexEncodedPublicKey
2019-11-15 11:37:36 +11:00
val serviceDataMessage = getDataMessage ( message )
2020-02-12 16:42:33 +11:00
val serviceContent = SignalServiceContent ( serviceDataMessage , senderHexEncodedPublicKey , SignalServiceAddress . DEFAULT _DEVICE _ID , message . timestamp , false , false )
2019-11-18 15:10:40 +11:00
if ( serviceDataMessage . quote . isPresent || ( serviceDataMessage . attachments . isPresent && serviceDataMessage . attachments . get ( ) . size > 0 ) || serviceDataMessage . previews . isPresent ) {
PushDecryptJob ( context ) . handleMediaMessage ( serviceContent , serviceDataMessage , Optional . absent ( ) , Optional . of ( message . serverID ) )
} else {
PushDecryptJob ( context ) . handleTextMessage ( serviceContent , serviceDataMessage , Optional . absent ( ) , Optional . of ( message . serverID ) )
2019-09-11 15:52:32 +10:00
}
2020-02-12 16:42:33 +11:00
// Update profile picture if needed
val senderAsRecipient = Recipient . from ( context , Address . fromSerialized ( senderHexEncodedPublicKey ) , false )
2020-02-12 12:26:27 +11:00
if ( message . profilePicture != null && message . profilePicture !! . url . isNotEmpty ( ) ) {
val profileKey = message . profilePicture !! . profileKey
val url = message . profilePicture !! . url
2020-02-12 16:42:33 +11:00
if ( senderAsRecipient . profileKey == null || ! MessageDigest . isEqual ( senderAsRecipient . profileKey , profileKey ) ) {
2019-11-28 12:49:33 +11:00
val database = DatabaseFactory . getRecipientDatabase ( context )
2020-02-12 16:42:33 +11:00
database . setProfileKey ( senderAsRecipient , profileKey )
ApplicationContext . getInstance ( context ) . jobManager . add ( RetrieveProfileAvatarJob ( senderAsRecipient , url ) )
2019-11-28 12:49:33 +11:00
}
2020-02-12 16:42:33 +11:00
} else if ( senderAsRecipient . profileAvatar . orEmpty ( ) . isNotEmpty ( ) ) {
// Clear the profile picture if we had a profile picture before and we're not friends with the person
val threadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( senderAsRecipient )
val friendRequestStatus = DatabaseFactory . getLokiThreadDatabase ( context ) . getFriendRequestStatus ( threadID )
2019-11-28 12:49:33 +11:00
if ( friendRequestStatus != LokiThreadFriendRequestStatus . FRIENDS ) {
2020-02-12 16:42:33 +11:00
ApplicationContext . getInstance ( context ) . jobManager . add ( RetrieveProfileAvatarJob ( senderAsRecipient , " " ) )
2019-11-28 12:49:33 +11:00
}
}
2019-09-04 11:11:48 +10:00
}
2019-10-15 13:39:17 +11:00
fun processOutgoingMessage ( message : LokiPublicChatMessage ) {
2019-09-04 11:11:48 +10:00
val messageServerID = message . serverID ?: return
2019-11-15 11:37:36 +11:00
val isDuplicate = DatabaseFactory . getLokiMessageDatabase ( context ) . getMessageID ( messageServerID ) != null
2019-09-04 11:11:48 +10:00
if ( isDuplicate ) { return }
2019-10-21 10:52:53 +11:00
if ( message . body . isEmpty ( ) && message . attachments . isEmpty ( ) && message . quote == null ) { return }
2020-02-12 16:42:33 +11:00
val userHexEncodedPublicKey = TextSecurePreferences . getLocalNumber ( context )
2019-11-15 11:37:36 +11:00
val dataMessage = getDataMessage ( message )
2020-02-12 16:42:33 +11:00
val transcript = SentTranscriptMessage ( userHexEncodedPublicKey , dataMessage . timestamp , dataMessage , dataMessage . expiresInSeconds . toLong ( ) , Collections . singletonMap ( userHexEncodedPublicKey , false ) )
2019-11-15 11:37:36 +11:00
transcript . messageServerID = messageServerID
2019-11-18 15:10:40 +11:00
if ( dataMessage . quote . isPresent || ( dataMessage . attachments . isPresent && dataMessage . attachments . get ( ) . size > 0 ) || dataMessage . previews . isPresent ) {
PushDecryptJob ( context ) . handleSynchronizeSentMediaMessage ( transcript )
} else {
PushDecryptJob ( context ) . handleSynchronizeSentTextMessage ( transcript )
2019-09-10 10:27:45 +10:00
}
2020-02-12 16:42:33 +11:00
// If we got a message from our master device then make sure our mapping stays in sync
2019-11-28 12:49:33 +11:00
val recipient = Recipient . from ( context , Address . fromSerialized ( message . hexEncodedPublicKey ) , false )
2020-02-12 12:26:27 +11:00
if ( recipient . isOurMasterDevice && message . profilePicture != null ) {
val profileKey = message . profilePicture !! . profileKey
val url = message . profilePicture !! . url
2019-11-28 12:49:33 +11:00
if ( recipient . profileKey == null || ! MessageDigest . isEqual ( recipient . profileKey , profileKey ) ) {
val database = DatabaseFactory . getRecipientDatabase ( context )
database . setProfileKey ( recipient , profileKey )
database . setProfileAvatar ( recipient , url )
2020-02-13 09:28:00 +11:00
ApplicationContext . getInstance ( context ) . updatePublicChatProfilePictureIfNeeded ( )
2019-11-28 12:49:33 +11:00
}
}
2019-09-04 11:11:48 +10:00
}
2019-11-18 13:12:57 +11:00
var userDevices = setOf < String > ( )
2019-11-18 11:55:16 +11:00
var uniqueDevices = setOf < String > ( )
2020-01-22 11:57:16 +11:00
val userPrivateKey = IdentityKeyUtil . getIdentityKeyPair ( context ) . privateKey . serialize ( )
val database = DatabaseFactory . getLokiAPIDatabase ( context )
2020-02-11 09:38:05 +11:00
LokiFileServerAPI . configure ( false , userHexEncodedPublicKey , userPrivateKey , database )
2020-03-24 11:31:01 +11:00
// Kovenant propagates a context to chained promises, so LokiPublicChatAPI.sharedContext should be used for all of the below
2020-03-20 13:08:12 +11:00
LokiDeviceLinkUtilities . getAllLinkedDeviceHexEncodedPublicKeys ( userHexEncodedPublicKey ) . bind ( LokiPublicChatAPI . sharedContext ) { devices ->
2019-11-18 13:12:57 +11:00
userDevices = devices
2019-11-18 11:55:16 +11:00
api . getMessages ( group . channel , group . server )
2020-03-20 13:08:12 +11:00
} . bind ( LokiPublicChatAPI . sharedContext ) { messages ->
2019-11-15 09:58:13 +11:00
if ( messages . isNotEmpty ( ) ) {
2020-03-20 13:08:12 +11:00
if ( messages . count ( ) == 1 ) {
Log . d ( " Loki " , " Fetched 1 new message. " )
} else {
Log . d ( " Loki " , " Fetched ${messages.count()} new messages. " )
}
2020-02-12 16:42:33 +11:00
// We need to fetch the device mapping for any devices we don't have
2019-11-18 11:55:16 +11:00
uniqueDevices = messages . map { it . hexEncodedPublicKey } . toSet ( )
2020-02-12 12:26:27 +11:00
val devicesToUpdate = uniqueDevices . filter { ! userDevices . contains ( it ) && LokiFileServerAPI . shared . hasDeviceLinkCacheExpired ( hexEncodedPublicKey = it ) }
2019-11-18 11:55:16 +11:00
if ( devicesToUpdate . isNotEmpty ( ) ) {
2020-02-12 12:26:27 +11:00
return @bind LokiFileServerAPI . shared . getDeviceLinks ( devicesToUpdate . toSet ( ) ) . then { messages }
2019-11-18 11:08:23 +11:00
}
2019-11-18 11:55:16 +11:00
}
Promise . of ( messages )
2020-03-27 10:35:44 +11:00
} . successBackground {
2019-11-18 11:55:16 +11:00
val newDisplayNameUpdatees = uniqueDevices . mapNotNull {
2020-02-12 16:42:33 +11:00
// This will return null if the current device is a master device
LokiDeviceLinkUtilities . getMasterHexEncodedPublicKey ( it ) . get ( )
2019-11-18 11:55:16 +11:00
} . toSet ( )
2020-02-12 16:42:33 +11:00
// Fetch the display names of the master devices
2019-11-18 11:55:16 +11:00
displayNameUpdatees = displayNameUpdatees . union ( newDisplayNameUpdatees )
2020-03-27 10:35:44 +11:00
} . successBackground { messages ->
2019-11-18 11:55:16 +11:00
// Process messages in the background
2020-03-27 10:35:44 +11:00
messages . forEach { message ->
if ( userDevices . contains ( message . hexEncodedPublicKey ) ) {
processOutgoingMessage ( message )
} else {
processIncomingMessage ( message )
2019-09-04 11:11:48 +10:00
}
2020-03-27 10:35:44 +11:00
}
2019-08-05 16:27:37 +10:00
} . fail {
2019-10-10 10:53:17 +11:00
Log . d ( " Loki " , " Failed to get messages for group chat with ID: ${group.channel} on server: ${group.server} . " )
2019-08-05 16:27:37 +10:00
}
}
2019-08-26 13:03:38 +10:00
2019-11-18 11:08:23 +11:00
private fun pollForDisplayNames ( ) {
if ( displayNameUpdatees . isEmpty ( ) ) { return }
2019-11-18 13:12:57 +11:00
val hexEncodedPublicKeys = displayNameUpdatees
2019-11-18 11:08:23 +11:00
displayNameUpdatees = setOf ( )
2020-03-27 10:35:44 +11:00
api . getDisplayNames ( hexEncodedPublicKeys , group . server ) . successBackground { mapping ->
for ( pair in mapping . entries ) {
val senderDisplayName = " ${pair.value} (... ${pair.key.takeLast(8)} ) "
DatabaseFactory . getLokiUserDatabase ( context ) . setServerDisplayName ( group . id , pair . key , senderDisplayName )
}
2019-11-18 11:08:23 +11:00
} . fail {
2019-11-18 13:12:57 +11:00
displayNameUpdatees = displayNameUpdatees . union ( hexEncodedPublicKeys )
2019-11-18 11:08:23 +11:00
}
}
2019-08-26 13:03:38 +10:00
private fun pollForDeletedMessages ( ) {
2019-10-10 10:53:17 +11:00
api . getDeletedMessageServerIDs ( group . channel , group . server ) . success { deletedMessageServerIDs ->
2020-03-27 10:35:44 +11:00
val lokiMessageDatabase = DatabaseFactory . getLokiMessageDatabase ( context )
val deletedMessageIDs = deletedMessageServerIDs . mapNotNull { lokiMessageDatabase . getMessageID ( it ) }
val smsMessageDatabase = DatabaseFactory . getSmsDatabase ( context )
val mmsMessageDatabase = DatabaseFactory . getMmsDatabase ( context )
deletedMessageIDs . forEach {
smsMessageDatabase . deleteMessage ( it )
mmsMessageDatabase . delete ( it )
}
2019-08-26 13:03:38 +10:00
} . fail {
2019-10-10 10:53:17 +11:00
Log . d ( " Loki " , " Failed to get deleted messages for group chat with ID: ${group.channel} on server: ${group.server} . " )
2019-08-26 13:03:38 +10:00
}
}
2019-09-02 16:42:08 +10:00
2019-09-12 16:42:52 +10:00
private fun pollForModerators ( ) {
2019-10-10 10:53:17 +11:00
api . getModerators ( group . channel , group . server )
2019-09-02 16:42:08 +10:00
}
2019-09-04 11:11:48 +10:00
// endregion
2019-08-05 16:27:37 +10:00
}