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
2020-05-14 13:52:20 +10:00
import nl.komponents.kovenant.functional.bind
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
2020-07-17 14:40:15 +10:00
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
2020-05-14 13:52:20 +10:00
import org.thoughtcrime.securesms.loki.utilities.successBackground
2019-11-28 12:49:33 +11:00
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-07-15 12:24:43 +10:00
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatAPI
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatMessage
2020-08-04 15:33:37 +10:00
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol
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
2020-07-15 14:26:20 +10:00
class PublicChatPoller ( private val context : Context , private val group : PublicChat ) {
2019-08-06 16:18:24 +10:00
private val handler = Handler ( )
private var hasStarted = false
2020-09-02 16:14:57 +10:00
private var isPollOngoing = false
2020-07-10 14:00:04 +10:00
public var isCaughtUp = false
2020-06-26 16:17:15 +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
2020-07-15 12:24:43 +10:00
private val api : PublicChatAPI
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 )
2020-07-15 12:24:43 +10:00
PublicChatAPI ( 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
2020-05-22 11:18:47 +10:00
private val pollForDeletedMessagesInterval : Long = 60 * 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
2020-07-15 12:24:43 +10:00
private fun getDataMessage ( message : PublicChatMessage ) : SignalServiceDataMessage {
2019-11-15 11:37:36 +11:00
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 ) {
2020-07-15 12:24:43 +10:00
SignalServiceDataMessage . Quote ( message . quote !! . quotedMessageTimestamp , SignalServiceAddress ( message . quote !! . quoteePublicKey ) , message . quote !! . quotedMessageBody , listOf ( ) )
2019-11-15 11:37:36 +11:00
} else {
null
}
val attachments = message . attachments . mapNotNull { attachment ->
2020-07-15 12:24:43 +10:00
if ( attachment . kind != PublicChatMessage . Attachment . Kind . Attachment ) { return @mapNotNull null }
2019-11-15 11:37:36 +11:00
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
}
2020-07-15 12:24:43 +10:00
val linkPreview = message . attachments . firstOrNull { it . kind == PublicChatMessage . Attachment . Kind . LinkPreview }
2019-11-15 11:37:36 +11:00
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 ( ) {
2020-07-15 12:24:43 +10:00
fun processIncomingMessage ( message : PublicChatMessage ) {
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
2020-08-04 15:33:37 +10:00
val masterHexEncodedPublicKey = MultiDeviceProtocol . shared . getMasterDevice ( message . senderPublicKey )
2020-02-12 16:42:33 +11:00
if ( masterHexEncodedPublicKey == null ) {
2020-08-04 15:33:37 +10:00
val senderDisplayName = " ${message.displayName} (... ${message.senderPublicKey.takeLast(8)} ) "
DatabaseFactory . getLokiUserDatabase ( context ) . setServerDisplayName ( group . id , message . senderPublicKey , senderDisplayName )
2019-11-18 11:08:23 +11:00
}
2020-08-04 15:33:37 +10:00
val senderHexEncodedPublicKey = masterHexEncodedPublicKey ?: message . senderPublicKey
2019-11-15 11:37:36 +11:00
val serviceDataMessage = getDataMessage ( message )
2020-08-27 10:49:15 +10:00
val serviceContent = SignalServiceContent ( serviceDataMessage , senderHexEncodedPublicKey , SignalServiceAddress . DEFAULT _DEVICE _ID , message . serverTimestamp , 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
}
}
2019-09-04 11:11:48 +10:00
}
2020-07-15 12:24:43 +10:00
fun processOutgoingMessage ( message : PublicChatMessage ) {
2019-09-04 11:11:48 +10:00
val messageServerID = message . serverID ?: return
2020-09-03 13:14:26 +10:00
val messageID = DatabaseFactory . getLokiMessageDatabase ( context ) . getMessageID ( messageServerID )
var isDuplicate = false
if ( messageID != null ) {
2020-09-03 17:23:23 +10:00
isDuplicate = DatabaseFactory . getMmsDatabase ( context ) . getThreadIdForMessage ( messageID ) > 0
|| DatabaseFactory . getSmsDatabase ( context ) . getThreadIdForMessage ( messageID ) > 0
2020-09-03 13:14:26 +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-09-02 14:44:15 +10:00
SessionMetaProtocol . dropFromTimestampCacheIfNeeded ( message . serverTimestamp )
2020-09-01 09:48:14 +10:00
val transcript = SentTranscriptMessage ( userHexEncodedPublicKey , message . serverTimestamp , 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
2020-08-04 15:33:37 +10:00
val recipient = Recipient . from ( context , Address . fromSerialized ( message . senderPublicKey ) , false )
2020-05-11 16:54:31 +10:00
if ( recipient . isUserMasterDevice && message . profilePicture != null ) {
2020-02-12 12:26:27 +11:00
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-05-14 09:35:34 +10:00
ApplicationContext . getInstance ( context ) . updateOpenGroupProfilePicturesIfNeeded ( )
2019-11-28 12:49:33 +11:00
}
}
2019-09-04 11:11:48 +10:00
}
2020-09-02 16:14:57 +10:00
if ( isPollOngoing ) { return }
isPollOngoing = true
2020-05-14 13:52:20 +10:00
val userDevices = MultiDeviceProtocol . shared . getAllLinkedDevices ( userHexEncodedPublicKey )
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 ( )
2020-05-14 13:52:20 +10:00
val apiDB = DatabaseFactory . getLokiAPIDatabase ( context )
2020-07-15 12:24:43 +10:00
FileServerAPI . configure ( userHexEncodedPublicKey , userPrivateKey , apiDB )
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-07-15 12:24:43 +10:00
api . getMessages ( group . channel , group . server ) . bind ( PublicChatAPI . sharedContext ) { messages ->
2020-08-04 15:33:37 +10:00
/ *
2019-11-15 09:58:13 +11:00
if ( messages . isNotEmpty ( ) ) {
2020-02-12 16:42:33 +11:00
// We need to fetch the device mapping for any devices we don't have
2020-08-04 15:33:37 +10:00
uniqueDevices = messages . map { it . senderPublicKey } . toSet ( )
2020-07-15 12:24:43 +10:00
val devicesToUpdate = uniqueDevices . filter { ! userDevices . contains ( it ) && FileServerAPI . shared . hasDeviceLinkCacheExpired ( publicKey = it ) }
2019-11-18 11:55:16 +11:00
if ( devicesToUpdate . isNotEmpty ( ) ) {
2020-07-15 12:24:43 +10:00
return @bind FileServerAPI . shared . getDeviceLinks ( devicesToUpdate . toSet ( ) ) . then { messages }
2019-11-18 11:08:23 +11:00
}
2019-11-18 11:55:16 +11:00
}
2020-08-04 15:33:37 +10:00
* /
2019-11-18 11:55:16 +11:00
Promise . of ( messages )
2020-03-27 10:35:44 +11:00
} . successBackground {
2020-08-04 15:33:37 +10:00
/ *
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
2020-05-14 13:52:20 +10:00
MultiDeviceProtocol . shared . getMasterDevice ( it )
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-08-04 15:33:37 +10:00
* /
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 ->
2020-08-04 15:33:37 +10:00
if ( userDevices . contains ( message . senderPublicKey ) ) {
2020-03-27 10:35:44 +11:00
processOutgoingMessage ( message )
} else {
processIncomingMessage ( message )
2019-09-04 11:11:48 +10:00
}
2020-03-27 10:35:44 +11:00
}
2020-07-08 11:30:00 +10:00
isCaughtUp = true
2020-09-02 16:14:57 +10:00
isPollOngoing = false
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} . " )
2020-09-02 16:14:57 +10:00
isPollOngoing = false
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
}