From 7c02bdae5a34392b8963f88294ae061ff31f5e8d Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:03:29 +1100 Subject: [PATCH 01/19] MessageSender to become dependency injected --- .../messaging/jobs/AttachmentDownloadJob.kt | 20 +-- .../messaging/jobs/AttachmentUploadJob.kt | 25 ++-- .../messaging/jobs/InviteContactsJob.kt | 40 ++++-- .../messaging/jobs/MessageSendJob.kt | 23 ++- .../jobs/SessionJobManagerFactories.kt | 10 +- .../sending_receiving/MessageSender.kt | 46 +++--- .../securesms/MediaPreviewActivity.kt | 7 +- .../DisappearingMessages.kt | 3 +- .../conversation/v2/ConversationActivityV2.kt | 16 ++- .../v2/utilities/ResendMessageUtilities.kt | 18 ++- .../securesms/database/ThreadDatabase.java | 6 +- .../securesms/groups/GroupLeavingWorker.kt | 5 +- .../securesms/groups/GroupManagerV2Impl.kt | 26 ++-- .../handler/RemoveGroupMemberHandler.kt | 3 +- .../securesms/media/MediaOverviewViewModel.kt | 5 +- .../AndroidAutoHeardReceiver.java | 17 ++- .../AndroidAutoReplyReceiver.java | 5 +- .../notifications/MarkReadProcessor.kt | 132 ++++++++++++++++++ .../notifications/MarkReadReceiver.kt | 113 +-------------- .../notifications/RemoteReplyReceiver.java | 4 +- .../repository/ConversationRepository.kt | 13 +- .../securesms/webrtc/CallManager.kt | 21 +-- 22 files changed, 316 insertions(+), 242 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index a6747b3b10..46f473e210 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -1,10 +1,8 @@ package org.session.libsession.messaging.jobs -import android.content.Context import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import dagger.hilt.android.qualifiers.ApplicationContext import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.database.MessageDataProvider @@ -16,7 +14,6 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.Address import org.session.libsession.utilities.DecodedAudio import org.session.libsession.utilities.InputStreamMediaDataSource @@ -252,21 +249,18 @@ class AttachmentDownloadJob @AssistedInject constructor( return KEY } - class DeserializeFactory(private val factory: Factory) : Job.DeserializeFactory { + @AssistedFactory + abstract class Factory : Job.DeserializeFactory { + abstract fun create( + @Assisted("attachmentID") attachmentID: Long, + mmsMessageId: Long + ): AttachmentDownloadJob override fun create(data: Data): AttachmentDownloadJob { - return factory.create( + return create( attachmentID = data.getLong(ATTACHMENT_ID_KEY), mmsMessageId = data.getLong(TS_INCOMING_MESSAGE_ID_KEY) ) } } - - @AssistedFactory - interface Factory { - fun create( - @Assisted("attachmentID") attachmentID: Long, - mmsMessageId: Long - ): AttachmentDownloadJob - } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index bd31a3c3f1..86ce01eec7 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -15,7 +15,6 @@ import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.Address import org.session.libsession.utilities.DecodedAudio import org.session.libsession.utilities.InputStreamMediaDataSource @@ -38,6 +37,7 @@ class AttachmentUploadJob @AssistedInject constructor( private val attachmentProcessor: AttachmentProcessor, private val preferences: TextSecurePreferences, private val fileServerApi: FileServerApi, + private val messageSender: MessageSender, ) : Job { override var delegate: JobDelegate? = null override var id: String? = null @@ -219,7 +219,7 @@ class AttachmentUploadJob @AssistedInject constructor( private fun failAssociatedMessageSendJob(e: Exception) { val messageSendJob = storage.getMessageSendJob(messageSendJobID) - MessageSender.handleFailedMessageSend(this.message, e) + messageSender.handleFailedMessageSend(this.message, e) if (messageSendJob != null) { storage.markJobAsFailedPermanently(messageSendJobID) } @@ -244,7 +244,14 @@ class AttachmentUploadJob @AssistedInject constructor( return KEY } - class DeserializeFactory(private val factory: Factory): Job.DeserializeFactory { + @AssistedFactory + abstract class Factory : Job.DeserializeFactory { + abstract fun create( + attachmentID: Long, + @Assisted("threadID") threadID: String, + message: Message, + messageSendJobID: String + ): AttachmentUploadJob override fun create(data: Data): AttachmentUploadJob? { val serializedMessage = data.getByteArray(MESSAGE_KEY) @@ -259,7 +266,7 @@ class AttachmentUploadJob @AssistedInject constructor( return null } input.close() - return factory.create( + return create( attachmentID = data.getLong(ATTACHMENT_ID_KEY), threadID = data.getString(THREAD_ID_KEY)!!, message = message, @@ -267,14 +274,4 @@ class AttachmentUploadJob @AssistedInject constructor( ) } } - - @AssistedFactory - interface Factory { - fun create( - attachmentID: Long, - @Assisted("threadID") threadID: String, - message: Message, - messageSendJobID: String - ): AttachmentUploadJob - } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt index 1a4c5cc9f9..70157d8ef1 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt @@ -2,6 +2,9 @@ package org.session.libsession.messaging.jobs import android.widget.Toast import com.google.protobuf.ByteString +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -16,13 +19,19 @@ import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.MessageAuthentication.buildGroupInviteSignature import org.session.libsession.snode.SnodeAPI +import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.getGroup import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log -class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array) : Job { +class InviteContactsJob @AssistedInject constructor( + @Assisted val groupSessionId: String, + @Assisted val memberSessionIds: Array, + private val configFactory: ConfigFactoryProtocol, + private val messageSender: MessageSender, +) : Job { companion object { const val KEY = "InviteContactJob" @@ -37,8 +46,7 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array< override val maxFailureCount: Int = 1 override suspend fun execute(dispatcherName: String) { - val configs = MessagingModuleConfiguration.shared.configFactory - val group = requireNotNull(configs.getGroup(AccountId(groupSessionId))) { + val group = requireNotNull(configFactory.getGroup(AccountId(groupSessionId))) { "Group must exist to invite" } @@ -54,7 +62,7 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array< runCatching { // Make the request for this member val memberId = AccountId(memberSessionId) - val (groupName, subAccount) = configs.withMutableGroupConfigs(sessionId) { configs -> + val (groupName, subAccount) = configFactory.withMutableGroupConfigs(sessionId) { configs -> configs.groupInfo.getName() to configs.groupKeys.makeSubAccount(memberSessionId) } @@ -76,14 +84,14 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array< sentTimestamp = timestamp } - MessageSender.sendNonDurably(update, Destination.Contact(memberSessionId), false) + messageSender.sendNonDurably(update, Destination.Contact(memberSessionId), false) } } } val results = memberSessionIds.zip(requests.awaitAll()) - configs.withMutableGroupConfigs(sessionId) { configs -> + configFactory.withMutableGroupConfigs(sessionId) { configs -> results.forEach { (memberSessionId, result) -> configs.groupMembers.get(memberSessionId)?.let { member -> if (result.isFailure) { @@ -96,8 +104,8 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array< } } - val groupName = configs.withGroupConfigs(sessionId) { it.groupInfo.getName() } - ?: configs.getGroup(sessionId)?.name + val groupName = configFactory.withGroupConfigs(sessionId) { it.groupInfo.getName() } + ?: configFactory.getGroup(sessionId)?.name // Gather all the exceptions, while keeping track of the invitee account IDs val failures = results.mapNotNull { (id, result) -> @@ -140,4 +148,20 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array< override fun getFactoryKey(): String = KEY + @AssistedFactory + abstract class Factory : Job.DeserializeFactory { + abstract fun create( + groupSessionId: String, + memberSessionIds: Array, + ): InviteContactsJob + + override fun create(data: Data): InviteContactsJob? { + val groupSessionId = data.getString(GROUP) ?: return null + val memberSessionIds = data.getStringArray(MEMBER) ?: return null + return create( + groupSessionId = groupSessionId, + memberSessionIds = memberSessionIds, + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index 238b7edb04..1cb9e88bfa 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -34,6 +34,7 @@ class MessageSendJob @AssistedInject constructor( private val messageDataProvider: MessageDataProvider, private val storage: StorageProtocol, private val configFactory: ConfigFactoryProtocol, + private val messageSender: MessageSender, ) : Job { object AwaitingAttachmentUploadException : Exception("Awaiting attachment upload.") @@ -97,7 +98,7 @@ class MessageSendJob @AssistedInject constructor( } } - MessageSender.sendNonDurably(this@MessageSendJob.message, destination, isSync) + messageSender.sendNonDurably(this@MessageSendJob.message, destination, isSync) this.handleSuccess(dispatcherName) statusCallback?.trySend(Result.success(Unit)) @@ -173,7 +174,14 @@ class MessageSendJob @AssistedInject constructor( return KEY } - class DeserializeFactory(private val factory: Factory) : Job.DeserializeFactory { + + @AssistedFactory + abstract class Factory : Job.DeserializeFactory { + abstract fun create( + message: Message, + destination: Destination, + statusCallback: SendChannel>? = null + ): MessageSendJob override fun create(data: Data): MessageSendJob? { val serializedMessage = data.getByteArray(MESSAGE_KEY) @@ -201,20 +209,11 @@ class MessageSendJob @AssistedInject constructor( } destinationInput.close() // Return - return factory.create( + return create( message = message, destination = destination, statusCallback = null ) } } - - @AssistedFactory - interface Factory { - fun create( - message: Message, - destination: Destination, - statusCallback: SendChannel>? = null - ): MessageSendJob - } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt b/app/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt index cd0468d9f2..e1780ce19f 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt @@ -8,18 +8,20 @@ class SessionJobManagerFactories @Inject constructor( private val batchFactory: BatchMessageReceiveJob.Factory, private val trimThreadFactory: TrimThreadJob.Factory, private val messageSendJobFactory: MessageSendJob.Factory, - private val deleteJobFactory: OpenGroupDeleteJob.Factory + private val deleteJobFactory: OpenGroupDeleteJob.Factory, + private val inviteContactsJobFactory: InviteContactsJob.Factory, ) { fun getSessionJobFactories(): Map> { return mapOf( - AttachmentDownloadJob.KEY to AttachmentDownloadJob.DeserializeFactory(attachmentDownloadJobFactory), - AttachmentUploadJob.KEY to AttachmentUploadJob.DeserializeFactory(attachmentUploadJobFactory), - MessageSendJob.KEY to MessageSendJob.DeserializeFactory(messageSendJobFactory), + AttachmentDownloadJob.KEY to attachmentDownloadJobFactory, + AttachmentUploadJob.KEY to attachmentUploadJobFactory, + MessageSendJob.KEY to messageSendJobFactory, NotifyPNServerJob.KEY to NotifyPNServerJob.DeserializeFactory(), TrimThreadJob.KEY to trimThreadFactory, BatchMessageReceiveJob.KEY to batchFactory, OpenGroupDeleteJob.KEY to deleteJobFactory, + InviteContactsJob.KEY to inviteContactsJobFactory, ) } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 0b5158ce5f..a9265313bd 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -11,8 +11,10 @@ import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISI import network.loki.messenger.libsession_util.Namespace import network.loki.messenger.libsession_util.util.BlindKeyAPI import network.loki.messenger.libsession_util.util.ExpiryMode -import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.database.MessageDataProvider +import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.applyExpiryMode @@ -31,9 +33,8 @@ import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI.nowWithOffset import org.session.libsession.snode.SnodeMessage import org.session.libsession.utilities.Address +import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.SSKEnvironment -import org.session.libsignal.crypto.PushTransportDetails -import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Hex @@ -47,7 +48,15 @@ import kotlin.coroutines.cancellation.CancellationException import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote -object MessageSender { +@Singleton +class MessageSender @Inject constructor( + private val storage: StorageProtocol, + private val configFactory: ConfigFactoryProtocol, + private val recipientRepository: RecipientRepository, + private val messageDataProvider: MessageDataProvider, + private val messageSendJobFactory: MessageSendJob.Factory, + private val messageExpirationManager: ExpiringMessageManager, +) { // Error sealed class Error(val description: String) : Exception(description) { @@ -81,8 +90,6 @@ object MessageSender { // One-on-One Chats & Closed Groups @Throws(Exception::class) fun buildWrappedMessageToSnode(destination: Destination, message: Message, isSyncMessage: Boolean): SnodeMessage { - val storage = MessagingModuleConfiguration.shared.storage - val configFactory = MessagingModuleConfiguration.shared.configFactory val userPublicKey = storage.getUserPublicKey() // Set the timestamp, sender and recipient val messageSendTime = nowWithOffset @@ -193,8 +200,6 @@ object MessageSender { // One-on-One Chats & Closed Groups private suspend fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false) = supervisorScope { - val configFactory = MessagingModuleConfiguration.shared.configFactory - // Set the failure handler (need it here already for precondition failure handling) fun handleFailure(error: Exception) { handleFailedMessageSend(message, error, isSyncMessage) @@ -275,7 +280,7 @@ object MessageSender { return message.run { (if (isSyncMessage && this is VisibleMessage) syncTarget else recipient) ?.let(Address::fromSerialized) - ?.let(MessagingModuleConfiguration.shared.recipientRepository::getRecipientSync) + ?.let(recipientRepository::getRecipientSync) ?.expiryMode ?.takeIf { it is ExpiryMode.AfterSend || isSyncMessage } ?.expiryMillis @@ -285,8 +290,6 @@ object MessageSender { // Open Groups private suspend fun sendToOpenGroupDestination(destination: Destination, message: Message) { - val storage = MessagingModuleConfiguration.shared.storage - val configFactory = MessagingModuleConfiguration.shared.configFactory if (message.sentTimestamp == null) { message.sentTimestamp = nowWithOffset } @@ -296,7 +299,7 @@ object MessageSender { message.blocksMessageRequests = !configs.userProfile.getCommunityMessageRequests() } } - val userEdKeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair()!! + val userEdKeyPair = storage.getUserED25519KeyPair()!! var serverCapabilities = listOf() var blindedPublicKey: ByteArray? = null when(destination) { @@ -459,7 +462,7 @@ object MessageSender { storage.updateSentTimestamp(messageId, message.sentTimestamp!!) // Start the disappearing messages timer if needed - SSKEnvironment.shared.messageExpirationManager.onMessageSent(message) + messageExpirationManager.onMessageSent(message) } ?: run { storage.updateReactionIfNeeded(message, message.sender?:userPublicKey, openGroupSentTimestamp) } @@ -483,12 +486,10 @@ object MessageSender { } fun handleFailedMessageSend(message: Message, error: Exception, isSyncMessage: Boolean = false) { - val storage = MessagingModuleConfiguration.shared.storage - val messageId = message.id ?: return // no need to handle if message is marked as deleted - if(MessagingModuleConfiguration.shared.messageDataProvider.isDeletedMessage(messageId)){ + if (messageDataProvider.isDeletedMessage(messageId)){ return } @@ -499,7 +500,6 @@ object MessageSender { // Convenience @JvmStatic fun send(message: VisibleMessage, address: Address, quote: SignalQuote?, linkPreview: SignalLinkPreview?) { - val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val messageId = message.id if (messageId?.mms == true) { message.attachmentIDs.addAll(messageDataProvider.getAttachmentIDsFor(messageId.id)) @@ -520,21 +520,21 @@ object MessageSender { @JvmStatic @JvmOverloads fun send(message: Message, address: Address, statusCallback: SendChannel>? = null) { - val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) + val threadID = storage.getThreadId(address) message.applyExpiryMode(address) message.threadID = threadID val destination = Destination.from(address) - val job = MessagingModuleConfiguration.shared.messageSendJobFactory.create(message, destination, statusCallback) + val job = messageSendJobFactory.create(message, destination, statusCallback) JobQueue.shared.add(job) // if we are sending a 'Note to Self' make sure it is not hidden if( message is VisibleMessage && - address.toString() == MessagingModuleConfiguration.shared.storage.getUserPublicKey() && + address.toString() == storage.getUserPublicKey() && // only show the NTS if it is currently marked as hidden - MessagingModuleConfiguration.shared.configFactory.withUserConfigs { it.userProfile.getNtsPriority() == PRIORITY_HIDDEN } + configFactory.withUserConfigs { it.userProfile.getNtsPriority() == PRIORITY_HIDDEN } ){ // update config in case it was marked as hidden there - MessagingModuleConfiguration.shared.configFactory.withMutableUserConfigs { + configFactory.withMutableUserConfigs { it.userProfile.setNtsPriority(PRIORITY_VISIBLE) } } @@ -547,7 +547,7 @@ object MessageSender { } suspend fun sendNonDurably(message: Message, address: Address, isSyncMessage: Boolean) { - val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) + val threadID = storage.getThreadId(address) message.threadID = threadID val destination = Destination.from(address) sendNonDurably(message, destination, isSyncMessage) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt index 93d5841385..a3444934fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt @@ -71,7 +71,7 @@ import network.loki.messenger.databinding.MediaViewPageBinding import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager import org.session.libsession.messaging.messages.control.DataExtractionNotification import org.session.libsession.messaging.messages.control.DataExtractionNotification.Kind.MediaSaved -import org.session.libsession.messaging.sending_receiving.MessageSender.send +import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.snode.SnodeAPI.nowWithOffset import org.session.libsession.utilities.Address @@ -136,6 +136,9 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), @Inject lateinit var recipientRepository: RecipientRepository + @Inject + lateinit var messageSender: MessageSender + override val applyDefaultWindowInsets: Boolean get() = false @@ -552,7 +555,7 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(), nowWithOffset ) ) - send(message, conversationAddress!!) + messageSender.send(message, conversationAddress!!) } @SuppressLint("StaticFieldLeak") diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt index ac4257cd92..fe43d12f71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt @@ -29,6 +29,7 @@ class DisappearingMessages @Inject constructor( private val storage: StorageProtocol, private val groupManagerV2: GroupManagerV2, private val clock: SnodeClock, + private val messageSender: MessageSender, ) { fun set(address: Address, mode: ExpiryMode, isGroup: Boolean) { storage.setExpirationConfiguration(address, mode) @@ -45,7 +46,7 @@ class DisappearingMessages @Inject constructor( } messageExpirationManager.insertExpirationTimerMessage(message) - MessageSender.send(message, address) + messageSender.send(message, address) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index ba0903f2b0..9d7059d622 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -264,6 +264,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, @Inject lateinit var openGroupManager: OpenGroupManager @Inject lateinit var attachmentDatabase: AttachmentDatabase @Inject lateinit var clock: SnodeClock + @Inject lateinit var messageSender: MessageSender + @Inject lateinit var resendMessageUtilities: ResendMessageUtilities @Inject @ManagerScope lateinit var scope: CoroutineScope @@ -1790,7 +1792,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } } } else { - MessageSender.send(reactionMessage, recipient.address) + messageSender.send(reactionMessage, recipient.address) } LoaderManager.getInstance(this).restartLoader(0, null, this) @@ -1843,7 +1845,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } } } else { - MessageSender.send(message, recipient.address) + messageSender.send(message, recipient.address) } LoaderManager.getInstance(this).restartLoader(0, null, this) } @@ -2137,7 +2139,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, ), false) waitForApprovalJobToBeSubmitted() - MessageSender.send(message, recipient.address) + messageSender.send(message, recipient.address) } // Send a typing stopped message typingStatusSender.onTypingStopped(viewModel.threadId) @@ -2215,7 +2217,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, waitForApprovalJobToBeSubmitted() - MessageSender.send(message, recipient.address, quote, linkPreview) + messageSender.send(message, recipient.address, quote, linkPreview) }.onFailure { withContext(Dispatchers.Main){ when (it) { @@ -2558,7 +2560,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, scope.launch { messages.iterator().forEach { messageRecord -> runCatching { - ResendMessageUtilities.resend( + resendMessageUtilities.resend( accountId, messageRecord, viewModel.blindedPublicKey, @@ -2576,7 +2578,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, scope.launch { messages.iterator().forEach { messageRecord -> runCatching { - ResendMessageUtilities.resend( + resendMessageUtilities.resend( accountId, messageRecord, viewModel.blindedPublicKey @@ -2731,7 +2733,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val timestamp = SnodeAPI.nowWithOffset val kind = DataExtractionNotification.Kind.MediaSaved(timestamp) val message = DataExtractionNotification(kind) - MessageSender.send(message, recipient.address) + messageSender.send(message, recipient.address) } private fun endActionMode() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt index 2f47041204..1e3fb60b8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt @@ -1,6 +1,6 @@ package org.thoughtcrime.securesms.conversation.v2.utilities -import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.visible.LinkPreview import org.session.libsession.messaging.messages.visible.OpenGroupInvitation @@ -12,8 +12,12 @@ import org.session.libsession.utilities.isGroupOrCommunity import org.session.libsession.utilities.toGroupString import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import javax.inject.Inject -object ResendMessageUtilities { +class ResendMessageUtilities @Inject constructor( + private val messageSender: MessageSender, + private val storage: StorageProtocol, +) { suspend fun resend(accountId: String?, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) { val recipient = messageRecord.recipient.address @@ -51,14 +55,14 @@ object ResendMessageUtilities { message.addSignalAttachments(messageRecord.slideDeck.asAttachments()) } val sentTimestamp = message.sentTimestamp - val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey() + val sender = storage.getUserPublicKey() if (sentTimestamp != null && sender != null) { if (isResync) { - MessagingModuleConfiguration.shared.storage.markAsResyncing(messageRecord.messageId) - MessageSender.sendNonDurably(message, Destination.from(recipient), isSyncMessage = true) + storage.markAsResyncing(messageRecord.messageId) + messageSender.sendNonDurably(message, Destination.from(recipient), isSyncMessage = true) } else { - MessagingModuleConfiguration.shared.storage.markAsSending(messageRecord.messageId) - MessageSender.send(message, recipient) + storage.markAsSending(messageRecord.messageId) + messageSender.send(message, recipient) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index ec0cdbf912..4bed3ca3c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; +import org.thoughtcrime.securesms.notifications.MarkReadProcessor; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.util.SharedConfigUtilsKt; @@ -239,6 +240,7 @@ public static void migrateLegacyCommunityAddresses(final SQLiteDatabase db) { private final Lazy<@NonNull MessageNotifier> messageNotifier; private final Lazy<@NonNull MmsDatabase> mmsDatabase; private final Lazy<@NonNull SmsDatabase> smsDatabase; + private final Lazy<@NonNull MarkReadProcessor> markReadProcessor; @Inject public ThreadDatabase(@dagger.hilt.android.qualifiers.ApplicationContext Context context, @@ -249,6 +251,7 @@ public ThreadDatabase(@dagger.hilt.android.qualifiers.ApplicationContext Context Lazy<@NonNull MessageNotifier> messageNotifier, Lazy<@NonNull MmsDatabase> mmsDatabase, Lazy<@NonNull SmsDatabase> smsDatabase, + Lazy<@NonNull MarkReadProcessor> markReadProcessor, TextSecurePreferences prefs, Json json) { super(context, databaseHelper); @@ -258,6 +261,7 @@ public ThreadDatabase(@dagger.hilt.android.qualifiers.ApplicationContext Context this.messageNotifier = messageNotifier; this.mmsDatabase = mmsDatabase; this.smsDatabase = smsDatabase; + this.markReadProcessor = markReadProcessor; this.json = json; this.prefs = prefs; @@ -768,7 +772,7 @@ public boolean isRead(long threadId) { public boolean markAllAsRead(long threadId, long lastSeenTime, boolean force, boolean updateNotifications) { if (mmsSmsDatabase.get().getConversationCount(threadId) <= 0 && !force) return false; List messages = setRead(threadId, lastSeenTime); - MarkReadReceiver.process(context, messages); + markReadProcessor.get().process(context, messages); if(updateNotifications) messageNotifier.get().updateNotification(context, threadId); return setLastSeen(threadId, lastSeenTime); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupLeavingWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupLeavingWorker.kt index 141cb8233b..476b70925d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupLeavingWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupLeavingWorker.kt @@ -42,6 +42,7 @@ class GroupLeavingWorker @AssistedInject constructor( private val groupScope: GroupScope, private val tokenFetcher: TokenFetcher, private val pushRegistryV2: PushRegistryV2, + private val messageSender: MessageSender, ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val groupId = requireNotNull(inputData.getString(KEY_GROUP_ID)) { @@ -98,7 +99,7 @@ class GroupLeavingWorker @AssistedInject constructor( val statusChannel = Channel>() // Always send a "XXX left" message to the group if we can - MessageSender.send( + messageSender.send( GroupUpdated( GroupUpdateMessage.newBuilder() .setMemberLeftNotificationMessage(DataMessage.GroupUpdateMemberLeftNotificationMessage.getDefaultInstance()) @@ -110,7 +111,7 @@ class GroupLeavingWorker @AssistedInject constructor( // If we are not the only admin, send a left message for other admin to handle the member removal // We'll have to wait for this message to be sent before going ahead to delete the group - MessageSender.send( + messageSender.send( GroupUpdated( GroupUpdateMessage.newBuilder() .setMemberLeftMessage(DataMessage.GroupUpdateMemberLeftMessage.getDefaultInstance()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index c5e3acf8dc..753e94098d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -88,6 +88,8 @@ class GroupManagerV2Impl @Inject constructor( private val scope: GroupScope, private val groupPollerManager: GroupPollerManager, private val recipientRepository: RecipientRepository, + private val messageSender: MessageSender, + private val inviteContactJobFactory: InviteContactsJob.Factory, ) : GroupManagerV2 { private val dispatcher = Dispatchers.Default @@ -201,7 +203,7 @@ class GroupManagerV2Impl @Inject constructor( // Invite members JobQueue.shared.add( - InviteContactsJob( + inviteContactJobFactory.create( groupSessionId = groupId.hexString, memberSessionIds = members.map { it.hexString }.toTypedArray() ) @@ -321,9 +323,9 @@ class GroupManagerV2Impl @Inject constructor( // Send the invitation message to the new members JobQueue.shared.add( - InviteContactsJob( - group.hexString, - newMembers.map { it.hexString }.toTypedArray() + inviteContactJobFactory.create( + groupSessionId = group.hexString, + memberSessionIds = newMembers.map { it.hexString }.toTypedArray() ) ) } @@ -355,7 +357,7 @@ class GroupManagerV2Impl @Inject constructor( storage.insertGroupInfoChange(updatedMessage, group) - MessageSender.send(updatedMessage, Address.fromSerialized(group.hexString)) + messageSender.send(updatedMessage, Address.fromSerialized(group.hexString)) } override suspend fun removeMembers( @@ -394,7 +396,7 @@ class GroupManagerV2Impl @Inject constructor( updateMessage ).apply { sentTimestamp = timestamp } - MessageSender.send(message, Address.fromSerialized(groupAccountId.hexString)) + messageSender.send(message, Address.fromSerialized(groupAccountId.hexString)) storage.insertGroupInfoChange(message, groupAccountId) } @@ -524,7 +526,7 @@ class GroupManagerV2Impl @Inject constructor( val promotionDeferred = members.associateWith { member -> async { // The promotion message shouldn't be persisted to avoid being retried automatically - MessageSender.sendNonDurably( + messageSender.sendNonDurably( message = promoteMessage, address = Address.fromSerialized(member.hexString), isSyncMessage = false, @@ -555,7 +557,7 @@ class GroupManagerV2Impl @Inject constructor( if (!isRepromote) { - MessageSender.sendAndAwait(message, Address.fromSerialized(group.hexString)) + messageSender.sendAndAwait(message, Address.fromSerialized(group.hexString)) } } } @@ -659,7 +661,7 @@ class GroupManagerV2Impl @Inject constructor( val responseMessage = GroupUpdated(responseData.build(), profile = storage.getUserProfile()) // this will fail the first couple of times :) runCatching { - MessageSender.sendNonDurably( + messageSender.sendNonDurably( responseMessage, Destination.ClosedGroup(group.groupAccountId), isSyncMessage = false @@ -926,7 +928,7 @@ class GroupManagerV2Impl @Inject constructor( } storage.insertGroupInfoChange(message, groupId) - MessageSender.sendAndAwait(message, Address.fromSerialized(groupId.hexString)) + messageSender.sendAndAwait(message, Address.fromSerialized(groupId.hexString)) } override suspend fun setDescription(groupId: AccountId, newDescription: String): Unit = @@ -1008,7 +1010,7 @@ class GroupManagerV2Impl @Inject constructor( sentTimestamp = timestamp } - MessageSender.sendAndAwait(message, Address.fromSerialized(groupId.hexString)) + messageSender.sendAndAwait(message, Address.fromSerialized(groupId.hexString)) } override suspend fun handleDeleteMemberContent( @@ -1141,7 +1143,7 @@ class GroupManagerV2Impl @Inject constructor( sentTimestamp = timestamp } - MessageSender.send(message, Address.fromSerialized(groupId.hexString)) + messageSender.send(message, Address.fromSerialized(groupId.hexString)) storage.deleteGroupInfoMessages(groupId, UpdateMessageData.Kind.GroupExpirationUpdated::class.java) storage.insertGroupInfoChange(message, groupId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/handler/RemoveGroupMemberHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/handler/RemoveGroupMemberHandler.kt index c118a0983a..09242a4967 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/handler/RemoveGroupMemberHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/handler/RemoveGroupMemberHandler.kt @@ -62,6 +62,7 @@ class RemoveGroupMemberHandler @Inject constructor( private val storage: StorageProtocol, private val groupScope: GroupScope, @ManagerScope scope: CoroutineScope, + private val messageSender: MessageSender, ) : OnAppStartupComponent { init { scope.launch { @@ -220,7 +221,7 @@ class RemoveGroupMemberHandler @Inject constructor( ): SnodeMessage { val timestamp = clock.currentTimeMills() - return MessageSender.buildWrappedMessageToSnode( + return messageSender.buildWrappedMessageToSnode( destination = Destination.ClosedGroup(groupAccountId), message = GroupUpdated( SignalServiceProtos.DataMessage.GroupUpdateMessage.newBuilder() diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt index 807fff6de5..6d8c645b3e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt @@ -57,7 +57,8 @@ class MediaOverviewViewModel @AssistedInject constructor( private val threadDatabase: ThreadDatabase, private val mediaDatabase: MediaDatabase, private val dateUtils: DateUtils, - private val recipientRepository: RecipientRepository, + recipientRepository: RecipientRepository, + private val messageSender: MessageSender, ) : AndroidViewModel(application) { private val timeBuckets by lazy { FixedTimeBuckets() } @@ -293,7 +294,7 @@ class MediaOverviewViewModel @AssistedInject constructor( val timestamp = SnodeAPI.nowWithOffset val kind = DataExtractionNotification.Kind.MediaSaved(timestamp) val message = DataExtractionNotification(kind) - MessageSender.send(message, address) + messageSender.send(message, address) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java index 2d22a13532..62173b3390 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java @@ -25,17 +25,24 @@ import androidx.core.app.NotificationManagerCompat; +import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.database.MarkedMessageInfo; +import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import java.util.LinkedList; import java.util.List; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; + /** * Marks an Android Auto as read after the driver have listened to it */ +@AndroidEntryPoint public class AndroidAutoHeardReceiver extends BroadcastReceiver { public static final String TAG = AndroidAutoHeardReceiver.class.getSimpleName(); @@ -43,6 +50,10 @@ public class AndroidAutoHeardReceiver extends BroadcastReceiver { public static final String THREAD_IDS_EXTRA = "car_heard_thread_ids"; public static final String NOTIFICATION_ID_EXTRA = "car_notification_id"; + @Inject MarkReadProcessor markReadProcessor; + @Inject MessageNotifier messageNotifier; + @Inject ThreadDatabase threadDb; + @SuppressLint("StaticFieldLeak") @Override public void onReceive(final Context context, Intent intent) @@ -63,13 +74,13 @@ protected Void doInBackground(Void... params) { for (long threadId : threadIds) { Log.i(TAG, "Marking meassage as read: " + threadId); - List messageIds = DatabaseComponent.get(context).threadDatabase().setRead(threadId, true); + List messageIds = threadDb.setRead(threadId, true); messageIdsCollection.addAll(messageIds); } - ApplicationContext.getInstance(context).getMessageNotifier().updateNotification(context); - MarkReadReceiver.process(context, messageIdsCollection); + messageNotifier.updateNotification(context); + markReadProcessor.process(messageIdsCollection); return null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java index 26561eee2a..4a72a4320a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java @@ -77,6 +77,9 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver { @Inject MessageNotifier messageNotifier; + @Inject + MarkReadProcessor markReadProcessor; + @SuppressLint("StaticFieldLeak") @Override public void onReceive(final Context context, Intent intent) @@ -129,7 +132,7 @@ protected Void doInBackground(Void... params) { List messageIds = threadDatabase.setRead(replyThreadId, true); messageNotifier.updateNotification(context); - MarkReadReceiver.process(context, messageIds); + markReadProcessor.process(messageIds); return null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt new file mode 100644 index 0000000000..ba0795b630 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadProcessor.kt @@ -0,0 +1,132 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.session.libsession.database.StorageProtocol +import org.session.libsession.database.userAuth +import org.session.libsession.messaging.messages.control.ReadReceipt +import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeAPI.nowWithOffset +import org.session.libsession.snode.SnodeClock +import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled +import org.session.libsession.utilities.associateByNotNull +import org.session.libsession.utilities.isGroupOrCommunity +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.recipients.RecipientData +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType +import org.thoughtcrime.securesms.database.MarkedMessageInfo +import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.RecipientRepository +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.model.content.DisappearingMessageUpdate +import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import javax.inject.Inject + +class MarkReadProcessor @Inject constructor( + @param:ApplicationContext private val context: Context, + private val recipientRepository: RecipientRepository, + private val messageSender: MessageSender, + private val mmsSmsDatabase: MmsSmsDatabase, + private val threadDb: ThreadDatabase, + private val storage: StorageProtocol, + private val snodeClock: SnodeClock, +) { + fun process( + markedReadMessages: List + ) { + if (markedReadMessages.isEmpty()) return + + sendReadReceipts( + markedReadMessages = markedReadMessages + ) + + + // start disappear after read messages except TimerUpdates in groups. + markedReadMessages + .asSequence() + .filter { it.expiryType == ExpiryType.AFTER_READ } + .filter { mmsSmsDatabase.getMessageById(it.expirationInfo.id)?.run { + (messageContent is DisappearingMessageUpdate) + && threadDb.getRecipientForThreadId(threadId)?.isGroupOrCommunity == true } == false + } + .forEach { + val db = if (it.expirationInfo.id.mms) { + DatabaseComponent.get(context).mmsDatabase() + } else { + DatabaseComponent.get(context).smsDatabase() + } + + db.markExpireStarted(it.expirationInfo.id.id, nowWithOffset) + } + + hashToDisappearAfterReadMessage(context, markedReadMessages)?.let { hashToMessages -> + GlobalScope.launch { + try { + shortenExpiryOfDisappearingAfterRead(hashToMessages) + } catch (e: Exception) { + Log.e(TAG, "Failed to fetch updated expiries and schedule deletion", e) + } + } + } + } + + private fun hashToDisappearAfterReadMessage( + context: Context, + markedReadMessages: List + ): Map? { + val loki = DatabaseComponent.get(context).lokiMessageDatabase() + + return markedReadMessages + .filter { it.expiryType == ExpiryType.AFTER_READ } + .associateByNotNull { it.expirationInfo.run { loki.getMessageServerHash(id) } } + .takeIf { it.isNotEmpty() } + } + + private fun shortenExpiryOfDisappearingAfterRead( + hashToMessage: Map + ) { + hashToMessage.entries + .groupBy( + keySelector = { it.value.expirationInfo.expiresIn }, + valueTransform = { it.key } + ).forEach { (expiresIn, hashes) -> + SnodeAPI.alterTtl( + messageHashes = hashes, + newExpiry = snodeClock.currentTimeMills() + expiresIn, + auth = checkNotNull(storage.userAuth) { "No authorized user" }, + shorten = true + ) + } + } + + private val Recipient.shouldSendReadReceipt: Boolean + get() = when (data) { + is RecipientData.Contact -> approved && !blocked + is RecipientData.Generic -> !isGroupOrCommunityRecipient && !blocked + else -> false + } + + private fun sendReadReceipts( + markedReadMessages: List + ) { + if (!isReadReceiptsEnabled(context)) return + + markedReadMessages.map { it.syncMessageId } + .filter { recipientRepository.getRecipientSync(it.address).shouldSendReadReceipt } + .groupBy { it.address } + .forEach { (address, messages) -> + messages.map { it.timetamp } + .let(::ReadReceipt) + .apply { sentTimestamp = snodeClock.currentTimeMills() } + .let { messageSender.send(it, address) } + } + } + + companion object { + private const val TAG = "MarkReadProcessor" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt index 1b8cb58d23..6b1cd80d4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.kt @@ -8,24 +8,8 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.session.libsession.database.StorageProtocol -import org.session.libsession.database.userAuth -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.MessagingModuleConfiguration.Companion.shared -import org.session.libsession.messaging.messages.control.ReadReceipt -import org.session.libsession.messaging.sending_receiving.MessageSender.send -import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.SnodeAPI.nowWithOffset import org.session.libsession.snode.SnodeClock -import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled -import org.session.libsession.utilities.associateByNotNull -import org.session.libsession.utilities.isGroupOrCommunity -import org.session.libsession.utilities.recipients.RecipientData -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType -import org.thoughtcrime.securesms.database.MarkedMessageInfo -import org.thoughtcrime.securesms.database.model.content.DisappearingMessageUpdate -import org.thoughtcrime.securesms.dependencies.DatabaseComponent import javax.inject.Inject @AndroidEntryPoint @@ -36,6 +20,7 @@ class MarkReadReceiver : BroadcastReceiver() { @Inject lateinit var clock: SnodeClock + override fun onReceive(context: Context, intent: Intent) { if (CLEAR_ACTION != intent.action) return val threadIds = intent.getLongArrayExtra(THREAD_IDS_EXTRA) ?: return @@ -59,101 +44,5 @@ class MarkReadReceiver : BroadcastReceiver() { const val THREAD_IDS_EXTRA = "thread_ids" const val NOTIFICATION_ID_EXTRA = "notification_id" - @JvmStatic - fun process( - context: Context, - markedReadMessages: List - ) { - if (markedReadMessages.isEmpty()) return - - sendReadReceipts(context, markedReadMessages) - - val mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase() - - val threadDb = DatabaseComponent.get(context).threadDatabase() - - // start disappear after read messages except TimerUpdates in groups. - markedReadMessages - .asSequence() - .filter { it.expiryType == ExpiryType.AFTER_READ } - .filter { mmsSmsDatabase.getMessageById(it.expirationInfo.id)?.run { - (messageContent is DisappearingMessageUpdate) - && threadDb.getRecipientForThreadId(threadId)?.isGroupOrCommunity == true } == false - } - .forEach { - val db = if (it.expirationInfo.id.mms) { - DatabaseComponent.get(context).mmsDatabase() - } else { - DatabaseComponent.get(context).smsDatabase() - } - - db.markExpireStarted(it.expirationInfo.id.id, nowWithOffset) - } - - hashToDisappearAfterReadMessage(context, markedReadMessages)?.let { hashToMessages -> - GlobalScope.launch { - try { - shortenExpiryOfDisappearingAfterRead(hashToMessages) - } catch (e: Exception) { - Log.e(TAG, "Failed to fetch updated expiries and schedule deletion", e) - } - } - } - } - - private fun hashToDisappearAfterReadMessage( - context: Context, - markedReadMessages: List - ): Map? { - val loki = DatabaseComponent.get(context).lokiMessageDatabase() - - return markedReadMessages - .filter { it.expiryType == ExpiryType.AFTER_READ } - .associateByNotNull { it.expirationInfo.run { loki.getMessageServerHash(id) } } - .takeIf { it.isNotEmpty() } - } - - private fun shortenExpiryOfDisappearingAfterRead( - hashToMessage: Map - ) { - hashToMessage.entries - .groupBy( - keySelector = { it.value.expirationInfo.expiresIn }, - valueTransform = { it.key } - ).forEach { (expiresIn, hashes) -> - SnodeAPI.alterTtl( - messageHashes = hashes, - newExpiry = nowWithOffset + expiresIn, - auth = checkNotNull(shared.storage.userAuth) { "No authorized user" }, - shorten = true - ) - } - } - - private val Recipient.shouldSendReadReceipt: Boolean - get() = when (data) { - is RecipientData.Contact -> approved && !blocked - is RecipientData.Generic -> !isGroupOrCommunityRecipient && !blocked - else -> false - } - - private fun sendReadReceipts( - context: Context, - markedReadMessages: List - ) { - if (!isReadReceiptsEnabled(context)) return - - val recipientRepository = MessagingModuleConfiguration.shared.recipientRepository - - markedReadMessages.map { it.syncMessageId } - .filter { recipientRepository.getRecipientSync(it.address)?.shouldSendReadReceipt == true } - .groupBy { it.address } - .forEach { (address, messages) -> - messages.map { it.timetamp } - .let(::ReadReceipt) - .apply { sentTimestamp = nowWithOffset } - .let { send(it, address) } - } - } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index 216eadaae0..cbcd32e353 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -76,6 +76,8 @@ public class RemoteReplyReceiver extends BroadcastReceiver { SnodeClock clock; @Inject RecipientRepository recipientRepository; + @Inject + MarkReadProcessor markReadProcessor; @SuppressLint("StaticFieldLeak") @Override @@ -129,7 +131,7 @@ protected Void doInBackground(Void... params) { List messageIds = threadDatabase.setRead(threadId, true); messageNotifier.updateNotification(context); - MarkReadReceiver.process(context, messageIds); + markReadProcessor.process(messageIds); return null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index 56b8d0bef7..c6a0fa7cb0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -144,6 +144,7 @@ class DefaultConversationRepository @Inject constructor( private val recipientDatabase: RecipientSettingsDatabase, private val recipientRepository: RecipientRepository, @param:ManagerScope private val scope: CoroutineScope, + private val messageSender: MessageSender, ) : ConversationRepository { override val conversationListAddressesFlow = configFactory @@ -285,7 +286,7 @@ class DefaultConversationRepository @Inject constructor( false ) - MessageSender.send(message, contact) + messageSender.send(message, contact) } } @@ -410,12 +411,12 @@ class DefaultConversationRepository @Inject constructor( // send an UnsendRequest to user's swarm buildUnsendRequest(message).let { unsendRequest -> - userAddress?.let { MessageSender.send(unsendRequest, it) } + userAddress?.let { messageSender.send(unsendRequest, it) } } // send an UnsendRequest to recipient's swarm buildUnsendRequest(message).let { unsendRequest -> - MessageSender.send(unsendRequest, recipient) + messageSender.send(unsendRequest, recipient) } } } @@ -428,7 +429,7 @@ class DefaultConversationRepository @Inject constructor( messages.forEach { message -> // send an UnsendRequest to group's swarm buildUnsendRequest(message).let { unsendRequest -> - MessageSender.send(unsendRequest, recipient) + messageSender.send(unsendRequest, recipient) } } } @@ -467,7 +468,7 @@ class DefaultConversationRepository @Inject constructor( // send an UnsendRequest to user's swarm buildUnsendRequest(message).let { unsendRequest -> - userAddress?.let { MessageSender.send(unsendRequest, it) } + userAddress?.let { messageSender.send(unsendRequest, it) } } } } @@ -551,7 +552,7 @@ class DefaultConversationRepository @Inject constructor( } withContext(Dispatchers.Default) { - MessageSender.send(message = MessageRequestResponse(true), address = recipient) + messageSender.send(message = MessageRequestResponse(true), address = recipient) // add a control message for our user storage.insertMessageRequestResponseFromYou(threadDb.getOrCreateThreadIdFor(recipient)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index 5a864b0f80..168e46276d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -74,6 +74,7 @@ class CallManager @Inject constructor( @param:ManagerScope private val scope: CoroutineScope, audioManager: AudioManagerCompat, private val storage: StorageProtocol, + private val messageSender: MessageSender, ): PeerConnection.Observer, SignalAudioManager.EventListener, CameraEventListener, DataChannel.Observer { @@ -340,7 +341,7 @@ class CallManager @Inject constructor( .also { scope.launch { runCatching { - MessageSender.sendNonDurably( + messageSender.sendNonDurably( it, currentRecipient, isSyncMessage = currentRecipient.isLocalNumber @@ -491,7 +492,7 @@ class CallManager @Inject constructor( Log.i("Loki", "Posting new answer") runCatching { - MessageSender.sendNonDurably( + messageSender.sendNonDurably( answerMessage, recipient, isSyncMessage = recipient.isLocalNumber @@ -545,7 +546,7 @@ class CallManager @Inject constructor( val userAddress = storage.getUserPublicKey() ?: throw NullPointerException("No user public key") runCatching { - MessageSender.sendNonDurably( + messageSender.sendNonDurably( answerMessage, Address.fromSerialized(userAddress), isSyncMessage = true @@ -553,7 +554,7 @@ class CallManager @Inject constructor( } runCatching { - MessageSender.sendNonDurably( + messageSender.sendNonDurably( CallMessage.answer( answer.description, callId @@ -609,7 +610,7 @@ class CallManager @Inject constructor( Log.d("Loki", "Sending pre-offer") try { - MessageSender.sendNonDurably( + messageSender.sendNonDurably( CallMessage.preOffer( callId ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber @@ -619,7 +620,7 @@ class CallManager @Inject constructor( Log.d("Loki", "Sending offer") postViewModelState(CallViewModel.State.CALL_OFFER_OUTGOING) - MessageSender.sendNonDurably(CallMessage.offer( + messageSender.sendNonDurably(CallMessage.offer( offer.description, callId ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) @@ -639,7 +640,7 @@ class CallManager @Inject constructor( stateProcessor.processEvent(Event.DeclineCall) { scope.launch { runCatching { - MessageSender.sendNonDurably( + messageSender.sendNonDurably( CallMessage.endCall(callId).applyExpiryMode(recipient), Address.fromSerialized(userAddress), isSyncMessage = true @@ -648,7 +649,7 @@ class CallManager @Inject constructor( } scope.launch { runCatching { - MessageSender.sendNonDurably( + messageSender.sendNonDurably( CallMessage.endCall(callId).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber @@ -680,7 +681,7 @@ class CallManager @Inject constructor( scope.launch { runCatching { - MessageSender.sendNonDurably( + messageSender.sendNonDurably( CallMessage.endCall(callId).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber @@ -919,7 +920,7 @@ class CallManager @Inject constructor( connection.setLocalDescription(offer) scope.launch { runCatching { - MessageSender.sendNonDurably( + messageSender.sendNonDurably( CallMessage.offer(offer.description, callId).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber From ebe8ab41531c71db9b79211ed2a857a2ce4d0af4 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:28:04 +1100 Subject: [PATCH 02/19] Message receiver to be dependency injected --- .../messaging/jobs/BatchMessageReceiveJob.kt | 4 +++- .../sending_receiving/MessageReceiver.kt | 21 ++++++++++++------- .../pollers/OpenGroupPoller.kt | 3 ++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index fc1bbfac42..ced99a7f2a 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -62,6 +62,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( private val messageNotifier: MessageNotifier, private val threadDatabase: ThreadDatabase, private val recipientRepository: RecipientRepository, + private val messageReceiver: MessageReceiver, ) : Job { override var delegate: JobDelegate? = null @@ -105,6 +106,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( fromCommunity = fromCommunity, threadDatabase = threadDatabase, recipientRepository = recipientRepository, + messageReceiver = messageReceiver, ) } @@ -157,7 +159,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( messages.forEach { messageParameters -> val (data, serverHash, openGroupMessageServerID) = messageParameters try { - val (message, proto) = MessageReceiver.parse( + val (message, proto) = messageReceiver.parse( data, openGroupMessageServerID, openGroupPublicKey = serverPublicKey, diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index 9e05ecce3e..e41ee5e500 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -1,7 +1,7 @@ package org.session.libsession.messaging.sending_receiving import network.loki.messenger.libsession_util.util.BlindKeyAPI -import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.messages.control.DataExtractionNotification @@ -13,7 +13,7 @@ import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.snode.SnodeAPI -import org.session.libsignal.crypto.PushTransportDetails +import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos.Envelope import org.session.libsignal.utilities.AccountId @@ -21,9 +21,15 @@ import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton import kotlin.math.abs -object MessageReceiver { +@Singleton +class MessageReceiver @Inject constructor( + private val configFactory: ConfigFactoryProtocol, + private val storage: StorageProtocol, +) { internal sealed class Error(message: String) : Exception(message) { object DuplicateMessage: Error("Duplicate message.") @@ -60,7 +66,6 @@ object MessageReceiver { currentClosedGroups: Set?, closedGroupSessionId: String? = null, ): Pair { - val storage = MessagingModuleConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey() val isOpenGroupMessage = (openGroupServerID != null) var plaintext: ByteArray? = null @@ -91,7 +96,7 @@ object MessageReceiver { plaintext = decryptionResult.first sender = decryptionResult.second } else { - val userX25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair() + val userX25519KeyPair = storage.getUserX25519KeyPair() val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), userX25519KeyPair) plaintext = decryptionResult.first sender = decryptionResult.second @@ -105,10 +110,10 @@ object MessageReceiver { sender = envelope.source groupPublicKey = hexEncodedGroupPublicKey } else { - if (!MessagingModuleConfiguration.shared.storage.isLegacyClosedGroup(hexEncodedGroupPublicKey)) { + if (!storage.isLegacyClosedGroup(hexEncodedGroupPublicKey)) { throw Error.InvalidGroupPublicKey } - val encryptionKeyPairs = MessagingModuleConfiguration.shared.storage.getClosedGroupEncryptionKeyPairs(hexEncodedGroupPublicKey) + val encryptionKeyPairs = storage.getClosedGroupEncryptionKeyPairs(hexEncodedGroupPublicKey) if (encryptionKeyPairs.isEmpty()) { throw Error.NoGroupKeyPair } @@ -172,7 +177,7 @@ object MessageReceiver { } val isUserBlindedSender = sender == openGroupPublicKey?.let { BlindKeyAPI.blind15KeyPairOrNull( - ed25519SecretKey = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair()!!.secretKey.data, + ed25519SecretKey = storage.getUserED25519KeyPair()!!.secretKey.data, serverPubKey = Hex.fromStringCondensed(it), ) }?.let { AccountId(IdPrefix.BLINDED, it.pubKey.data).hexString } diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index a6c56bb47c..3638d2f493 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -75,6 +75,7 @@ class OpenGroupPoller @AssistedInject constructor( private val trimThreadJobFactory: TrimThreadJob.Factory, private val openGroupDeleteJobFactory: OpenGroupDeleteJob.Factory, private val communityDatabase: CommunityDatabase, + private val messageReceiver: MessageReceiver, @Assisted private val server: String, @Assisted private val scope: CoroutineScope, @Assisted private val pollerSemaphore: Semaphore, @@ -339,7 +340,7 @@ class OpenGroupPoller @AssistedInject constructor( .setSource(it.sender) .build() try { - val (message, proto) = MessageReceiver.parse( + val (message, proto) = messageReceiver.parse( envelope.toByteArray(), null, fromOutbox, From 12e04060d473c09b142ad26bbe6651ebca9ed437 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:26:00 +1100 Subject: [PATCH 03/19] Changes to use kotlinx deserialization for some snode API calls --- .../sending_receiving/pollers/Poller.kt | 89 +++++---- .../org/session/libsession/snode/SnodeAPI.kt | 178 ++---------------- .../libsession/snode/model/BatchResponse.kt | 18 +- .../snode/model/MessageResponses.kt | 54 +++--- .../utilities/ConfigFactoryProtocol.kt | 3 +- .../securesms/configs/ConfigUploader.kt | 6 +- .../securesms/dependencies/ConfigFactory.kt | 12 +- .../securesms/groups/GroupPoller.kt | 75 ++++---- 8 files changed, 153 insertions(+), 282 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 13dfeea206..873ae274e0 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -27,19 +27,18 @@ import kotlinx.coroutines.supervisorScope import network.loki.messenger.libsession_util.Namespace import org.session.libsession.database.StorageProtocol import org.session.libsession.database.userAuth -import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters -import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeClock +import org.session.libsession.snode.model.RetrieveMessageResponse import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigMessage import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.UserConfigType import org.session.libsignal.database.LokiAPIDatabaseProtocol -import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Snode import org.thoughtcrime.securesms.util.AppVisibilityManager @@ -58,6 +57,7 @@ class Poller @AssistedInject constructor( private val appVisibilityManager: AppVisibilityManager, private val networkConnectivity: NetworkConnectivity, private val batchMessageReceiveJobFactory: BatchMessageReceiveJob.Factory, + private val snodeClock: SnodeClock, @Assisted scope: CoroutineScope ) { private val userPublicKey: String @@ -208,38 +208,58 @@ class Poller @AssistedInject constructor( } } - private fun processPersonalMessages(snode: Snode, rawMessages: RawResponse) { - val messages = SnodeAPI.parseRawMessagesResponse(rawMessages, snode, userPublicKey) - val parameters = messages.map { (envelope, serverHash) -> - MessageReceiveParameters(envelope.toByteArray(), serverHash = serverHash) - } - parameters.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { chunk -> - JobQueue.shared.add(batchMessageReceiveJobFactory.create( - messages = chunk, - fromCommunity = null - )) + private fun processPersonalMessages(snode: Snode, messages: List) { + if (messages.isEmpty()) { + return } + + lokiApiDatabase.setLastMessageHashValue( + snode = snode, + publicKey = userPublicKey, + newValue = messages.maxBy { it.timestamp }.hash, + namespace = Namespace.DEFAULT() + ) + + SnodeAPI.removeDuplicates( + publicKey = userPublicKey, + messages = messages, + messageHashGetter = { it.hash }, + namespace = Namespace.DEFAULT(), + updateStoredHashes = true + ).asSequence() + .map { msg -> + MessageReceiveParameters( + data = msg.data, + serverHash = msg.hash, + ) + } + .chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER) + .forEach { chunk -> + JobQueue.shared.add(batchMessageReceiveJobFactory.create( + messages = chunk, + fromCommunity = null + )) + } } - private fun processConfig(snode: Snode, rawMessages: RawResponse, forConfig: UserConfigType) { - Log.d(TAG, "Received ${rawMessages.size} messages for $forConfig") - val messages = rawMessages["messages"] as? List<*> + private fun processConfig(snode: Snode, messages: List, forConfig: UserConfigType) { + Log.d(TAG, "Received ${messages.size} messages for $forConfig") val namespace = forConfig.namespace - val processed = if (!messages.isNullOrEmpty()) { - SnodeAPI.updateLastMessageHashValueIfPossible(snode, userPublicKey, messages, namespace) + val processed = if (messages.isNotEmpty()) { + lokiApiDatabase.setLastMessageHashValue( + snode = snode, + publicKey = userPublicKey, + newValue = messages.maxBy { it.timestamp }.hash, + namespace = namespace + ) SnodeAPI.removeDuplicates( publicKey = userPublicKey, messages = messages, - messageHashGetter = { (it as? Map<*, *>)?.get("hash") as? String }, + messageHashGetter = { it.hash }, namespace = namespace, updateStoredHashes = true - ).mapNotNull { rawMessageAsJSON -> - rawMessageAsJSON as Map<*, *> // removeDuplicates should have ensured this is always a map - val hashValue = rawMessageAsJSON["hash"] as? String ?: return@mapNotNull null - val b64EncodedBody = rawMessageAsJSON["data"] as? String ?: return@mapNotNull null - val timestamp = rawMessageAsJSON["t"] as? Long ?: SnodeAPI.nowWithOffset - val body = Base64.decode(b64EncodedBody) - ConfigMessage(data = body, hash = hashValue, timestamp = timestamp) + ).map { m -> + ConfigMessage(data = m.data, hash = m.hash, timestamp = m.timestamp.toEpochMilli()) } } else emptyList() @@ -261,7 +281,7 @@ class Poller @AssistedInject constructor( private suspend fun poll(snode: Snode, pollOnlyUserProfileConfig: Boolean) = supervisorScope { - val userAuth = requireNotNull(MessagingModuleConfiguration.shared.storage.userAuth) + val userAuth = requireNotNull(storage.userAuth) // Get messages call wrapped in an async val fetchMessageTask = if (!pollOnlyUserProfileConfig) { @@ -280,7 +300,7 @@ class Poller @AssistedInject constructor( snode = snode, publicKey = userPublicKey, request = request, - responseType = Map::class.java + responseType = RetrieveMessageResponse.serializer() ) } } @@ -314,7 +334,12 @@ class Poller @AssistedInject constructor( this.async { type to runCatching { - SnodeAPI.sendBatchRequest(snode, userPublicKey, request, Map::class.java) + SnodeAPI.sendBatchRequest( + snode = snode, + publicKey = userPublicKey, + request = request, + responseType = RetrieveMessageResponse.serializer() + ) } } } @@ -329,7 +354,7 @@ class Poller @AssistedInject constructor( SnodeAPI.buildAuthenticatedAlterTtlBatchRequest( messageHashes = hashesToExtend.toList(), auth = userAuth, - newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, + newExpiry = snodeClock.currentTimeMills() + 14.days.inWholeMilliseconds, extend = true ) ) @@ -350,7 +375,7 @@ class Poller @AssistedInject constructor( continue } - processConfig(snode, result.getOrThrow(), configType) + processConfig(snode, result.getOrThrow().messages, configType) } // Process the messages if we requested them @@ -359,7 +384,7 @@ class Poller @AssistedInject constructor( if (result.isFailure) { Log.e(TAG, "Error while fetching messages", result.exceptionOrNull()) } else { - processPersonalMessages(snode, result.getOrThrow()) + processPersonalMessages(snode, result.getOrThrow().messages) } } } diff --git a/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt index 7b471a216c..977ea0fb45 100644 --- a/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -3,7 +3,6 @@ package org.session.libsession.snode import android.os.SystemClock -import com.fasterxml.jackson.databind.JsonNode import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -12,20 +11,19 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.selects.onTimeout import kotlinx.coroutines.selects.select +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject import network.loki.messenger.libsession_util.ED25519 import network.loki.messenger.libsession_util.Hash import network.loki.messenger.libsession_util.SessionEncrypt import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.all import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map -import nl.komponents.kovenant.unwrap import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.snode.model.BatchResponse import org.session.libsession.snode.model.StoreMessageResponse import org.session.libsession.snode.utilities.asyncPromise @@ -37,17 +35,13 @@ import org.session.libsession.utilities.toByteArray import org.session.libsignal.crypto.secureRandom import org.session.libsignal.crypto.shuffledRandom import org.session.libsignal.database.LokiAPIDatabaseProtocol -import org.session.libsignal.protos.SignalServiceProtos -import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.Broadcaster import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.prettifiedDescription -import org.session.libsignal.utilities.retryIfNeeded import org.session.libsignal.utilities.retryWithUniformInterval import java.util.Locale import kotlin.collections.component1 @@ -355,49 +349,6 @@ object SnodeAPI { } } - /** - * Retrieve messages from the swarm. - * - * @param snode The swarm service where you want to retrieve messages from. It can be a swarm for a specific user or a group. Call [getSingleTargetSnode] to get a swarm node. - * @param auth The authentication data required to retrieve messages. This can be a user or group authentication data. - * @param namespace The namespace of the messages you want to retrieve. Default is 0. - */ - fun getRawMessages( - snode: Snode, - auth: SwarmAuth, - namespace: Int = 0 - ): RawResponsePromise { - val parameters = buildAuthenticatedParameters( - namespace = namespace, - auth = auth, - verificationData = { ns, t -> "${Snode.Method.Retrieve.rawValue}$ns$t" } - ) { - put( - "last_hash", - database.getLastMessageHashValue(snode, auth.accountId.hexString, namespace).orEmpty() - ) - } - - // Make the request - return invoke(Snode.Method.Retrieve, snode, parameters, auth.accountId.hexString) - } - - fun getUnauthenticatedRawMessages( - snode: Snode, - publicKey: String, - namespace: Int = 0 - ): RawResponsePromise { - val parameters = buildMap { - put("last_hash", database.getLastMessageHashValue(snode, publicKey, namespace).orEmpty()) - put("pubkey", publicKey) - if (namespace != 0) { - put("namespace", namespace) - } - } - - return invoke(Snode.Method.Retrieve, snode, parameters, publicKey) - } - fun buildAuthenticatedStoreBatchInfo( namespace: Int, message: SnodeMessage, @@ -541,40 +492,11 @@ object SnodeAPI { ) } - @Suppress("UNCHECKED_CAST") - fun getRawBatchResponse( - snode: Snode, - publicKey: String, - requests: List, - sequence: Boolean = false - ): RawResponsePromise { - val parameters = buildMap { this["requests"] = requests } - return invoke( - if (sequence) Snode.Method.Sequence else Snode.Method.Batch, - snode, - parameters, - publicKey - ).success { rawResponses -> - rawResponses[KEY_RESULTS].let { it as List } - .asSequence() - .filter { it[KEY_CODE] as? Int != 200 } - .forEach { response -> - Log.w("Loki", "response code was not 200") - handleSnodeError( - response[KEY_CODE] as? Int ?: 0, - response[KEY_BODY] as? Map<*, *>, - snode, - publicKey - ) - } - } - } - private data class RequestInfo( val snode: Snode, val publicKey: String, val request: SnodeBatchRequestInfo, - val responseType: Class<*>, + val responseType: DeserializationStrategy<*>, val callback: SendChannel>, val requestTime: Long = SystemClock.elapsedRealtime(), ) @@ -641,7 +563,8 @@ object SnodeAPI { throw BatchResponse.Error(resp) } - JsonUtil.fromJson(resp.body, req.responseType) + MessagingModuleConfiguration.shared.json.decodeFromJsonElement( + req.responseType, resp.body)!! } runCatching { @@ -664,7 +587,7 @@ object SnodeAPI { snode: Snode, publicKey: String, request: SnodeBatchRequestInfo, - responseType: Class, + responseType: DeserializationStrategy, ): T { val callback = Channel>(capacity = 1) @Suppress("UNCHECKED_CAST") @@ -689,8 +612,8 @@ object SnodeAPI { snode: Snode, publicKey: String, request: SnodeBatchRequestInfo, - ): JsonNode { - return sendBatchRequest(snode, publicKey, request, JsonNode::class.java) + ): JsonElement { + return sendBatchRequest(snode, publicKey, request, JsonElement.serializer()) } suspend fun getBatchResponse( @@ -712,8 +635,8 @@ object SnodeAPI { if (firstError != null) { handleSnodeError( statusCode = firstError.code, - json = if (firstError.body.isObject) { - JsonUtil.fromJson(firstError.body, Map::class.java) + json = if (firstError.body is JsonObject) { + JsonUtil.fromJson(firstError.body.toString(), Map::class.java) } else { null }, @@ -724,30 +647,6 @@ object SnodeAPI { } } - fun getExpiries( - messageHashes: List, - auth: SwarmAuth, - ): RawResponsePromise { - val hashes = messageHashes.takeIf { it.size != 1 } - ?: (messageHashes + "///////////////////////////////////////////") // TODO remove this when bug is fixed on nodes. - return scope.retrySuspendAsPromise(maxRetryCount) { - val params = buildAuthenticatedParameters( - auth = auth, - namespace = null, - verificationData = { _, t -> buildString { - append(Snode.Method.GetExpiries.rawValue) - append(t) - hashes.forEach(this::append) - } }, - ) { - this["messages"] = hashes - } - - val snode = getSingleTargetSnode(auth.accountId.hexString).await() - invoke(Snode.Method.GetExpiries, snode, params, auth.accountId.hexString).await() - } - } - fun alterTtl( auth: SwarmAuth, messageHashes: List, @@ -790,12 +689,6 @@ object SnodeAPI { } } - fun getMessages(auth: SwarmAuth): MessageListPromise = scope.retrySuspendAsPromise(maxRetryCount) { - val snode = getSingleTargetSnode(auth.accountId.hexString).await() - val resp = getRawMessages(snode, auth).await() - parseRawMessagesResponse(resp, snode, auth.accountId.hexString) - } - fun getNetworkTime(snode: Snode): Promise, Exception> = invoke(Snode.Method.Info, snode, emptyMap()).map { rawResponse -> val timestamp = rawResponse["timestamp"] as? Long ?: -1 @@ -845,7 +738,7 @@ object SnodeAPI { params = params, namespace = namespace ), - responseType = StoreMessageResponse::class.java + responseType = StoreMessageResponse.serializer() ) } } @@ -959,27 +852,6 @@ object SnodeAPI { ) } - fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true, updateStoredHashes: Boolean = true, decrypt: ((ByteArray) -> Pair?)? = null): List> = - (rawResponse["messages"] as? List<*>)?.let { messages -> - if (updateLatestHash) updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace) - removeDuplicates( - publicKey = publicKey, - messages = parseEnvelopes(messages, decrypt), - messageHashGetter = { it.second }, - namespace = namespace, - updateStoredHashes = updateStoredHashes - ) - } ?: listOf() - - fun updateLastMessageHashValueIfPossible(snode: Snode, publicKey: String, rawMessages: List<*>, namespace: Int) { - val lastMessageAsJSON = rawMessages.lastOrNull() as? Map<*, *> - val hashValue = lastMessageAsJSON?.get("hash") as? String - when { - hashValue != null -> database.setLastMessageHashValue(snode, publicKey, hashValue, namespace) - rawMessages.isNotEmpty() -> Log.d("Loki", "Failed to update last message hash value from: ${rawMessages.prettifiedDescription()}.") - } - } - /** * * @@ -1018,31 +890,6 @@ object SnodeAPI { } } - private fun parseEnvelopes(rawMessages: List<*>, decrypt: ((ByteArray)->Pair?)?): List> { - return rawMessages.mapNotNull { rawMessage -> - val rawMessageAsJSON = rawMessage as? Map<*, *> - val base64EncodedData = rawMessageAsJSON?.get("data") as? String - val data = base64EncodedData?.let { Base64.decode(it) } - if (data != null) { - try { - if (decrypt != null) { - val (decrypted, sender) = decrypt(data)!! - val envelope = SignalServiceProtos.Envelope.parseFrom(decrypted).toBuilder() - envelope.source = sender.hexString - Pair(envelope.build(), rawMessageAsJSON["hash"] as? String) - } - else Pair(MessageWrapper.unwrap(data), rawMessageAsJSON["hash"] as? String) - } catch (e: Exception) { - Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.", e) - null - } - } else { - Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.") - null - } - } - } - @Suppress("UNCHECKED_CAST") private fun parseDeletions(userPublicKey: String, timestamp: Long, rawResponse: RawResponse): Map = (rawResponse["swarm"] as? Map)?.mapValuesNotNull { (hexSnodePublicKey, rawJSON) -> @@ -1111,5 +958,4 @@ object SnodeAPI { // Type Aliases typealias RawResponse = Map<*, *> -typealias MessageListPromise = Promise>, Exception> typealias RawResponsePromise = Promise diff --git a/app/src/main/java/org/session/libsession/snode/model/BatchResponse.kt b/app/src/main/java/org/session/libsession/snode/model/BatchResponse.kt index 723abfc79f..d3fa2acd19 100644 --- a/app/src/main/java/org/session/libsession/snode/model/BatchResponse.kt +++ b/app/src/main/java/org/session/libsession/snode/model/BatchResponse.kt @@ -1,15 +1,15 @@ package org.session.libsession.snode.model -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.JsonNode +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement -data class BatchResponse @JsonCreator constructor( - @param:JsonProperty("results") val results: List, -) { - data class Item @JsonCreator constructor( - @param:JsonProperty("code") val code: Int, - @param:JsonProperty("body") val body: JsonNode, +@Serializable + +data class BatchResponse(val results: List, ) { + @Serializable + data class Item( + val code: Int, + val body: JsonElement, ) { val isSuccessful: Boolean get() = code in 200..299 diff --git a/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt b/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt index b9173e1462..e4d432376f 100644 --- a/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt +++ b/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt @@ -1,41 +1,33 @@ package org.session.libsession.snode.model import android.util.Base64 -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.util.StdConverter +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.session.libsession.utilities.serializable.InstantAsMillisSerializer +import java.time.Instant -data class StoreMessageResponse @JsonCreator constructor( - @JsonProperty("hash") val hash: String, - @JsonProperty("t") val timestamp: Long, +@Serializable +data class StoreMessageResponse( + val hash: String, + @Serializable(InstantAsMillisSerializer::class) + @SerialName("t") val timestamp: Instant, ) -class RetrieveMessageResponse @JsonCreator constructor( - @JsonProperty("messages") - // Apply converter to the element so that if one of the message fails to deserialize, it will - // be a null value instead of failing the whole list. - @JsonDeserialize(contentConverter = RetrieveMessageConverter::class) - val messages: List, +@Serializable +data class RetrieveMessageResponse( + val messages: List, ) { - class Message( + @Serializable + data class Message( val hash: String, - val timestamp: Long?, - val data: ByteArray, - ) -} - -internal class RetrieveMessageConverter : StdConverter() { - override fun convert(value: JsonNode?): RetrieveMessageResponse.Message? { - value ?: return null - - val hash = value.get("hash")?.asText()?.takeIf { it.isNotEmpty() } ?: return null - val timestamp = value.get("t")?.asLong()?.takeIf { it > 0 } - val data = runCatching { - Base64.decode(value.get("data")?.asText().orEmpty(), Base64.DEFAULT) - }.getOrNull() ?: return null - - return RetrieveMessageResponse.Message(hash, timestamp, data) + @Serializable(InstantAsMillisSerializer::class) + @SerialName("t") + val timestamp: Instant, + @SerialName("data") + val dataB64: String? = null, + ) { + val data: ByteArray by lazy(LazyThreadSafetyMode.NONE) { + Base64.decode(dataB64, Base64.DEFAULT) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt b/app/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt index d01e013bd8..37545d464b 100644 --- a/app/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt +++ b/app/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt @@ -29,6 +29,7 @@ import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.snode.SwarmAuth import org.session.libsignal.utilities.AccountId +import java.time.Instant interface ConfigFactoryProtocol { val configUpdateNotifications: Flow @@ -103,7 +104,7 @@ class ConfigMessage( data class ConfigPushResult( val hashes: List, - val timestamp: Long + val timestamp: Instant ) enum class UserConfigType(val namespace: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigUploader.kt b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigUploader.kt index d7300a1e4d..2fa7ba3202 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigUploader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigUploader.kt @@ -250,7 +250,7 @@ class ConfigUploader @Inject constructor( ), auth ), - responseType = StoreMessageResponse::class.java + responseType = StoreMessageResponse.serializer() ).let(::listOf).toConfigPushResult() } @@ -284,7 +284,7 @@ class ConfigUploader @Inject constructor( val pendingConfig = configs.groupKeys.pendingConfig() if (pendingConfig != null) { for (hash in hashes) { - configs.groupKeys.loadKey(pendingConfig, hash, timestamp) + configs.groupKeys.loadKey(pendingConfig, hash, timestamp.toEpochMilli()) } } } @@ -329,7 +329,7 @@ class ConfigUploader @Inject constructor( ), auth, ), - responseType = StoreMessageResponse::class.java + responseType = StoreMessageResponse.serializer() ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt index 8b421e3644..d41121879c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -390,7 +390,7 @@ class ConfigFactory @Inject constructor( // We need to persist the data to the database to save timestamp after the push val userAccountId = requiresCurrentUserAccountId() for ((variant, data, timestamp) in dump) { - configDatabase.storeConfig(variant, userAccountId.hexString, data, timestamp) + configDatabase.storeConfig(variant, userAccountId.hexString, data, timestamp.toEpochMilli()) } } @@ -412,11 +412,11 @@ class ConfigFactory @Inject constructor( if (pendingConfig != null) { for (hash in hashes) { configs.groupKeys.loadKey( - pendingConfig, - hash, - timestamp, - configs.groupInfo.pointer, - configs.groupMembers.pointer + message = pendingConfig, + hash = hash, + timestampMs = timestamp.toEpochMilli(), + infoPtr = configs.groupInfo.pointer, + membersPtr = configs.groupMembers.pointer ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index df0b085e30..a1360fc30d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -6,6 +6,7 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.delay @@ -23,7 +24,6 @@ import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters import org.session.libsession.messaging.messages.Destination -import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeClock import org.session.libsession.snode.model.BatchResponse @@ -244,8 +244,8 @@ class GroupPoller @AssistedInject constructor( namespace = Namespace.REVOKED_GROUP_MESSAGES(), maxSize = null, ), - RetrieveMessageResponse::class.java - ).messages.filterNotNull() + RetrieveMessageResponse.serializer() + ).messages } if (configHashesToExtends.isNotEmpty() && adminKey != null) { @@ -281,7 +281,7 @@ class GroupPoller @AssistedInject constructor( namespace = Namespace.GROUP_MESSAGES(), maxSize = null, ), - responseType = Map::class.java + responseType = RetrieveMessageResponse.serializer() ) } @@ -304,7 +304,7 @@ class GroupPoller @AssistedInject constructor( namespace = ns, maxSize = null, ), - responseType = RetrieveMessageResponse::class.java + responseType = RetrieveMessageResponse.serializer() ).messages.filterNotNull() } } @@ -314,7 +314,7 @@ class GroupPoller @AssistedInject constructor( // must be processed first. pollingTasks += "polling and handling group config keys and messages" to async { val result = runCatching { - val (keysMessage, infoMessage, membersMessage) = groupConfigRetrieval.map { it.await() } + val (keysMessage, infoMessage, membersMessage) = groupConfigRetrieval.awaitAll() handleGroupConfigMessages(keysMessage, infoMessage, membersMessage) saveLastMessageHash(snode, keysMessage, Namespace.GROUP_KEYS()) saveLastMessageHash(snode, infoMessage, Namespace.GROUP_INFO()) @@ -325,7 +325,7 @@ class GroupPoller @AssistedInject constructor( } val regularMessages = groupMessageRetrieval.await() - handleMessages(regularMessages, snode) + handleMessages(regularMessages.messages, snode) } // Revoke message must be handled regardless, and at the end @@ -370,7 +370,7 @@ class GroupPoller @AssistedInject constructor( if (badResponse != null) { Log.e(TAG, "Group polling failed due to a server error", badResponse) - pollState.swarmNodes -= currentSnode!! + pollState.swarmNodes -= currentSnode } } } @@ -386,7 +386,7 @@ class GroupPoller @AssistedInject constructor( } private fun RetrieveMessageResponse.Message.toConfigMessage(): ConfigMessage { - return ConfigMessage(hash, data, timestamp ?: clock.currentTimeMills()) + return ConfigMessage(hash, data, timestamp.toEpochMilli()) } private fun saveLastMessageHash( @@ -432,34 +432,41 @@ class GroupPoller @AssistedInject constructor( ) } - private fun handleMessages(body: RawResponse, snode: Snode) { - val messages = configFactoryProtocol.withGroupConfigs(groupId) { - SnodeAPI.parseRawMessagesResponse( - rawResponse = body, - snode = snode, - publicKey = groupId.hexString, - decrypt = { data -> - val (decrypted, sender) = it.groupKeys.decrypt(data) ?: return@parseRawMessagesResponse null - decrypted to AccountId(sender) - }, - namespace = Namespace.GROUP_MESSAGES(), - ) + private fun handleMessages(messages: List, snode: Snode) { + if (messages.isEmpty()) { + return } - val parameters = messages.map { (envelope, serverHash) -> - MessageReceiveParameters( - envelope.toByteArray(), - serverHash = serverHash, - closedGroup = Destination.ClosedGroup(groupId.hexString) - ) - } + lokiApiDatabase.setLastMessageHashValue( + snode = snode, + publicKey = groupId.hexString, + newValue = messages.maxBy { it.timestamp }.hash, + namespace = Namespace.GROUP_MESSAGES() + ) - parameters.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { chunk -> - JobQueue.shared.add(batchMessageReceiveJobFactory.create( - messages = chunk, - fromCommunity = null - )) - } + SnodeAPI.removeDuplicates( + publicKey = groupId.hexString, + namespace = Namespace.GROUP_MESSAGES(), + messages = messages, + messageHashGetter = { it.hash }, + updateStoredHashes = true + ).asSequence() + .map { msg -> + MessageReceiveParameters( + data = msg.data, + serverHash = msg.hash, + closedGroup = Destination.ClosedGroup(groupId.hexString), + ) + } + .chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER) + .forEach { chunk -> + JobQueue.shared.add( + batchMessageReceiveJobFactory.create( + messages = chunk, + fromCommunity = null + ) + ) + } if (messages.isNotEmpty()) { Log.d(TAG, "Received and handled ${messages.size} group messages") From ba0ea5e1230079dae8d6ec0d15817a26853712ef Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:58:19 +1100 Subject: [PATCH 04/19] Integrating WIP --- .../libsession/database/StorageProtocol.kt | 2 - .../messaging/jobs/BatchMessageReceiveJob.kt | 73 +++--- .../messaging/messages/Destination.kt | 16 +- .../messages/visible/ParsedMessage.kt | 2 +- .../messaging/open_groups/OpenGroupMessage.kt | 7 - .../sending_receiving/MessageReceiver.kt | 213 ++++++++++-------- .../sending_receiving/MessageSender.kt | 198 ++++++---------- .../pollers/OpenGroupPoller.kt | 64 ++---- .../messaging/utilities/MessageWrapper.kt | 75 +----- .../crypto/PushTransportDetails.java | 55 ----- .../securesms/database/Storage.kt | 10 - gradle/libs.versions.toml | 2 +- 12 files changed, 277 insertions(+), 440 deletions(-) delete mode 100644 app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java diff --git a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt index f30b4aef98..f77b398165 100644 --- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -113,8 +113,6 @@ interface StorageProtocol { fun setActive(groupID: String, value: Boolean) fun removeMember(groupID: String, member: Address) fun updateMembers(groupID: String, members: List
) - fun getAllLegacyGroupPublicKeys(): Set - fun getAllActiveClosedGroupPublicKeys(): Set fun addClosedGroupPublicKey(groupPublicKey: String) fun removeClosedGroupPublicKey(groupPublicKey: String) fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index ced99a7f2a..b6581fb2ab 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -36,6 +36,7 @@ import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.UserConfigType import org.session.libsignal.protos.UtilProtos +import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.ThreadDatabase @@ -43,7 +44,7 @@ import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.ReactionRecord import kotlin.math.max -data class MessageReceiveParameters( +class MessageReceiveParameters( val data: ByteArray, val serverHash: String? = null, val openGroupMessageServerID: Long? = null, @@ -152,29 +153,49 @@ class BatchMessageReceiveJob @AssistedInject constructor( suspend fun executeAsync(dispatcherName: String) { val threadMap = mutableMapOf>>() val localUserPublicKey = storage.getUserPublicKey() - val serverPublicKey = fromCommunity?.let { storage.getOpenGroupPublicKey(it.serverUrl) } - val currentClosedGroups = storage.getAllActiveClosedGroupPublicKeys() // parse and collect IDs - messages.forEach { messageParameters -> - val (data, serverHash, openGroupMessageServerID) = messageParameters + messages.forEach { params -> try { - val (message, proto) = messageReceiver.parse( - data, - openGroupMessageServerID, - openGroupPublicKey = serverPublicKey, - currentClosedGroups = currentClosedGroups, - closedGroupSessionId = messageParameters.closedGroup?.publicKey - ) - message.serverHash = serverHash - val parsedParams = ParsedMessage(messageParameters, message, proto) + val parsedParams = when { + fromCommunity != null -> { + messageReceiver.parseCommunityMessage( + data = params.data, + messageServerId = requireNotNull(params.openGroupMessageServerID) { + "Open group message must have server ID" + }, + communityServerPubKeyHex = requireNotNull(storage.getOpenGroupPublicKey(fromCommunity.serverUrl)) { + "Given message doesn't have a valid community server public key or the group has been deleted" + } + ) + } + + params.closedGroup != null -> { + messageReceiver.parseGroupMessage( + data = params.data, + serverHash = requireNotNull(params.serverHash) { + "Closed group message must have server hash" + }, + groupId = AccountId(params.closedGroup.publicKey) + ) + } + + else -> { + messageReceiver.parse1o1Message( + data = params.data, + serverHash = requireNotNull(params.serverHash) { + "1on1 message must have server hash" + } + ) + } + } - if(isHidden(message)) return@forEach + if(isHidden(parsedParams.message)) return@forEach val threadAddress = when { fromCommunity != null -> fromCommunity - message.groupPublicKey != null -> message.groupPublicKey!!.toAddress() - else -> message.senderOrSync.toAddress() + parsedParams.message.groupPublicKey != null -> parsedParams.message.groupPublicKey!!.toAddress() + else -> parsedParams.message.senderOrSync.toAddress() } as Address.Conversable val threadID = if (shouldCreateThread(parsedParams)) { @@ -194,12 +215,12 @@ class BatchMessageReceiveJob @AssistedInject constructor( } else { Log.e(TAG, "Couldn't receive message, failed (id: $id)", e) - failures += messageParameters + failures += params } } else -> { Log.e(TAG, "Couldn't receive message, failed (id: $id)", e) - failures += messageParameters + failures += params } } } @@ -218,9 +239,9 @@ class BatchMessageReceiveJob @AssistedInject constructor( val communityReactions = mutableMapOf>() - messages.forEach { (parameters, message, proto) -> + messages.forEach { msg -> try { - when (message) { + when (val message = msg.message) { is VisibleMessage -> { val isUserBlindedSender = message.sender == handlerContext.userBlindedKey @@ -231,7 +252,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( } val messageId = receivedMessageHandler.handleVisibleMessage( message = message, - proto = proto, + proto = msg.proto, context = handlerContext, runThreadUpdate = false, runProfileUpdate = true @@ -244,11 +265,11 @@ class BatchMessageReceiveJob @AssistedInject constructor( ) } - parameters.openGroupMessageServerID?.let { + msg.parameters.openGroupMessageServerID?.let { constructReactionRecords( openGroupMessageServerID = it, context = handlerContext, - reactions = parameters.reactions, + reactions = msg.parameters.reactions, out = communityReactions ) } @@ -265,7 +286,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( else -> receivedMessageHandler.handle( message = message, - proto = proto, + proto = msg.proto, threadId = threadId, threadAddress = threadAddress ) @@ -276,7 +297,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( Log.e(TAG, "Message failed permanently (id: $id)", e) } else { Log.e(TAG, "Message failed (id: $id)", e) - failures += parameters + failures += msg.parameters } } } diff --git a/app/src/main/java/org/session/libsession/messaging/messages/Destination.kt b/app/src/main/java/org/session/libsession/messaging/messages/Destination.kt index bb8c29a0ce..5ddbad9fa1 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/Destination.kt +++ b/app/src/main/java/org/session/libsession/messaging/messages/Destination.kt @@ -8,12 +8,6 @@ sealed class Destination { data class Contact(var publicKey: String) : Destination() { internal constructor(): this("") } - data class LegacyClosedGroup(var groupPublicKey: String) : Destination() { - internal constructor(): this("") - } - data class LegacyOpenGroup(var roomToken: String, var server: String) : Destination() { - internal constructor(): this("", "") - } data class ClosedGroup(var publicKey: String): Destination() { internal constructor(): this("") } @@ -39,9 +33,6 @@ sealed class Destination { is Address.Standard -> { Contact(address.address) } - is Address.LegacyGroup -> { - LegacyClosedGroup(address.groupPublicKeyHex) - } is Address.Community -> { OpenGroup(roomToken = address.room, server = address.serverUrl, fileIds = fileIds) } @@ -63,9 +54,10 @@ sealed class Destination { is Address.Group -> { ClosedGroup(address.accountId.hexString) } - else -> { - throw Exception("TODO: Handle legacy closed groups.") - } + + is Address.Blinded, + is Address.LegacyGroup, + is Address.Unknown -> error("Unsupported address as destination: $address") } } } diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt index b0c5ced904..5f0ecdd2a7 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt +++ b/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt @@ -4,7 +4,7 @@ import org.session.libsession.messaging.jobs.MessageReceiveParameters import org.session.libsession.messaging.messages.Message import org.session.libsignal.protos.SignalServiceProtos -data class ParsedMessage( +class ParsedMessage( val parameters: MessageReceiveParameters, val message: Message, val proto: SignalServiceProtos.Content diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt index 625061bf0e..5b493a1b42 100644 --- a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt +++ b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt @@ -4,8 +4,6 @@ import network.loki.messenger.libsession_util.ED25519 import network.loki.messenger.libsession_util.util.BlindKeyAPI import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability -import org.session.libsignal.crypto.PushTransportDetails -import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64.decode import org.session.libsignal.utilities.Log @@ -84,9 +82,4 @@ data class OpenGroupMessage( base64EncodedSignature?.let { json["signature"] = it } return json } - - fun toProto(): SignalServiceProtos.Content { - val data = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody) - return SignalServiceProtos.Content.parseFrom(data) - } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index e41ee5e500..af58ba9719 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -1,5 +1,6 @@ package org.session.libsession.messaging.sending_receiving +import network.loki.messenger.libsession_util.protocol.SessionProtocol import network.loki.messenger.libsession_util.util.BlindKeyAPI import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.messages.Message @@ -11,8 +12,10 @@ import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.ReadReceipt import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.UnsendRequest +import org.session.libsession.messaging.messages.visible.ParsedMessage import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos.Envelope @@ -29,6 +32,7 @@ import kotlin.math.abs class MessageReceiver @Inject constructor( private val configFactory: ConfigFactoryProtocol, private val storage: StorageProtocol, + private val snodeClock: SnodeClock, ) { internal sealed class Error(message: String) : Exception(message) { @@ -57,7 +61,46 @@ class MessageReceiver @Inject constructor( } } - internal fun parse( + fun parse1o1Message( + data: ByteArray, + serverHash: String, + ): ParsedMessage { + + SessionProtocol.decodeEnvelope() + TODO() + + message.serverHash = serverHash + } + + fun parseGroupMessage( + data: ByteArray, + serverHash: String, + groupId: AccountId, + ): ParsedMessage { + TODO() + + message.serverHash = serverHash + } + + fun parseCommunityMessage( + data: ByteArray, + messageServerId: Long, + communityServerPubKeyHex: String, + ): ParsedMessage { + TODO() + } + + fun parseCommunityInboxMessage( + data: ByteArray, + isOutgoing: Boolean, + otherBlindedPublicKey: String, + communityServerPubKeyHex: String, + ): ParsedMessage { + TODO() + } + + private fun parse( + proto: SignalServiceProtos.Content, data: ByteArray, openGroupServerID: Long?, isOutgoing: Boolean? = null, @@ -66,86 +109,85 @@ class MessageReceiver @Inject constructor( currentClosedGroups: Set?, closedGroupSessionId: String? = null, ): Pair { - val userPublicKey = storage.getUserPublicKey() - val isOpenGroupMessage = (openGroupServerID != null) - var plaintext: ByteArray? = null - var sender: String? = null - var groupPublicKey: String? = null - // Parse the envelope - val envelope = Envelope.parseFrom(data) ?: throw Error.InvalidMessage - // Decrypt the contents - val envelopeContent = envelope.content ?: run { - throw Error.NoData - } - - if (isOpenGroupMessage) { - plaintext = envelopeContent.toByteArray() - sender = envelope.source - } else { - when (envelope.type) { - SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> { - if (IdPrefix.fromValue(envelope.source)?.isBlinded() == true) { - openGroupPublicKey ?: throw Error.InvalidGroupPublicKey - otherBlindedPublicKey ?: throw Error.DecryptionFailed - val decryptionResult = MessageDecrypter.decryptBlinded( - envelopeContent.toByteArray(), - isOutgoing ?: false, - otherBlindedPublicKey, - openGroupPublicKey - ) - plaintext = decryptionResult.first - sender = decryptionResult.second - } else { - val userX25519KeyPair = storage.getUserX25519KeyPair() - val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), userX25519KeyPair) - plaintext = decryptionResult.first - sender = decryptionResult.second - } - } - SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE -> { - val hexEncodedGroupPublicKey = closedGroupSessionId ?: envelope.source - val sessionId = AccountId(hexEncodedGroupPublicKey) - if (sessionId.prefix == IdPrefix.GROUP) { - plaintext = envelopeContent.toByteArray() - sender = envelope.source - groupPublicKey = hexEncodedGroupPublicKey - } else { - if (!storage.isLegacyClosedGroup(hexEncodedGroupPublicKey)) { - throw Error.InvalidGroupPublicKey - } - val encryptionKeyPairs = storage.getClosedGroupEncryptionKeyPairs(hexEncodedGroupPublicKey) - if (encryptionKeyPairs.isEmpty()) { - throw Error.NoGroupKeyPair - } - // Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than - // likely be the one we want) but try older ones in case that didn't work) - var encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex) - fun decrypt() { - try { - val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), encryptionKeyPair) - plaintext = decryptionResult.first - sender = decryptionResult.second - } catch (e: Exception) { - if (encryptionKeyPairs.isNotEmpty()) { - encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex) - decrypt() - } else { - Log.e("Loki", "Failed to decrypt group message", e) - throw e - } - } - } - groupPublicKey = hexEncodedGroupPublicKey - decrypt() - } - } - else -> { - throw Error.UnknownEnvelopeType - } - } - } - // Parse the proto - val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext)) +// val userPublicKey = storage.getUserPublicKey() +// val isOpenGroupMessage = (openGroupServerID != null) +// var plaintext: ByteArray? = null +// var sender: String? = null +// var groupPublicKey: String? = null +// // Parse the envelope +// val envelope = Envelope.parseFrom(data) ?: throw Error.InvalidMessage +// // Decrypt the contents +// val envelopeContent = envelope.content ?: run { +// throw Error.NoData +// } +// +// if (isOpenGroupMessage) { +// plaintext = envelopeContent.toByteArray() +// sender = envelope.source +// } else { +// when (envelope.type) { +// SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> { +// if (IdPrefix.fromValue(envelope.source)?.isBlinded() == true) { +// openGroupPublicKey ?: throw Error.InvalidGroupPublicKey +// otherBlindedPublicKey ?: throw Error.DecryptionFailed +// val decryptionResult = MessageDecrypter.decryptBlinded( +// envelopeContent.toByteArray(), +// isOutgoing ?: false, +// otherBlindedPublicKey, +// openGroupPublicKey +// ) +// plaintext = decryptionResult.first +// sender = decryptionResult.second +// } else { +// val userX25519KeyPair = storage.getUserX25519KeyPair() +// val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), userX25519KeyPair) +// plaintext = decryptionResult.first +// sender = decryptionResult.second +// } +// } +// SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE -> { +// val hexEncodedGroupPublicKey = closedGroupSessionId ?: envelope.source +// val sessionId = AccountId(hexEncodedGroupPublicKey) +// if (sessionId.prefix == IdPrefix.GROUP) { +// plaintext = envelopeContent.toByteArray() +// sender = envelope.source +// groupPublicKey = hexEncodedGroupPublicKey +// } else { +// if (!storage.isLegacyClosedGroup(hexEncodedGroupPublicKey)) { +// throw Error.InvalidGroupPublicKey +// } +// val encryptionKeyPairs = storage.getClosedGroupEncryptionKeyPairs(hexEncodedGroupPublicKey) +// if (encryptionKeyPairs.isEmpty()) { +// throw Error.NoGroupKeyPair +// } +// // Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than +// // likely be the one we want) but try older ones in case that didn't work) +// var encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex) +// fun decrypt() { +// try { +// val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), encryptionKeyPair) +// plaintext = decryptionResult.first +// sender = decryptionResult.second +// } catch (e: Exception) { +// if (encryptionKeyPairs.isNotEmpty()) { +// encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex) +// decrypt() +// } else { +// Log.e("Loki", "Failed to decrypt group message", e) +// throw e +// } +// } +// } +// groupPublicKey = hexEncodedGroupPublicKey +// decrypt() +// } +// } +// else -> { +// throw Error.UnknownEnvelopeType +// } +// } +// } +// // Parse the proto // Verify the signature timestamp inside the content is the same as in envelope. // If the message is from an open group, 6 hours of difference is allowed. @@ -196,7 +238,7 @@ class MessageReceiver @Inject constructor( message.sender = sender message.recipient = userPublicKey message.sentTimestamp = envelope.timestampMs - message.receivedTimestamp = if (envelope.hasServerTimestampMs()) envelope.serverTimestampMs else SnodeAPI.nowWithOffset + message.receivedTimestamp = if (envelope.hasServerTimestampMs()) envelope.serverTimestampMs else snodeClock.currentTimeMills() message.groupPublicKey = groupPublicKey message.openGroupServerMessageID = openGroupServerID // Validate @@ -205,12 +247,7 @@ class MessageReceiver @Inject constructor( if (!isValid) { throw Error.InvalidMessage } - // If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp - // will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround - // for this issue. - if (groupPublicKey != null && groupPublicKey !in (currentClosedGroups ?: emptySet()) && IdPrefix.fromValue(groupPublicKey) != IdPrefix.GROUP) { - throw Error.NoGroupThread - } + if (storage.isDuplicateMessage(envelope.timestampMs)) { throw Error.DuplicateMessage } storage.addReceivedMessageTimestamp(envelope.timestampMs) // Return diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index a9265313bd..a35bc1fa95 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -1,7 +1,6 @@ package org.session.libsession.messaging.sending_receiving import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.launch @@ -9,6 +8,7 @@ import kotlinx.coroutines.supervisorScope import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE import network.loki.messenger.libsession_util.Namespace +import network.loki.messenger.libsession_util.protocol.SessionProtocol import network.loki.messenger.libsession_util.util.BlindKeyAPI import network.loki.messenger.libsession_util.util.ExpiryMode import org.session.libsession.database.MessageDataProvider @@ -28,22 +28,21 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability import org.session.libsession.messaging.open_groups.OpenGroupMessage -import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI.nowWithOffset import org.session.libsession.snode.SnodeMessage import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsession.utilities.SSKEnvironment import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.defaultRequiresAuth -import org.session.libsignal.utilities.hasNamespaces -import org.session.libsignal.utilities.hexEncodedPublicKey +import org.thoughtcrime.securesms.database.RecipientRepository +import org.thoughtcrime.securesms.service.ExpiringMessageManager import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.cancellation.CancellationException import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote @@ -80,7 +79,7 @@ class MessageSender @Inject constructor( // Convenience suspend fun sendNonDurably(message: Message, destination: Destination, isSyncMessage: Boolean) { - return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) { + return if (destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) { sendToOpenGroupDestination(destination, message) } else { sendToSnodeDestination(destination, message, isSyncMessage) @@ -91,6 +90,9 @@ class MessageSender @Inject constructor( @Throws(Exception::class) fun buildWrappedMessageToSnode(destination: Destination, message: Message, isSyncMessage: Boolean): SnodeMessage { val userPublicKey = storage.getUserPublicKey() + val userEd25519PrivKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.data) { + "Missing user key" + } // Set the timestamp, sender and recipient val messageSendTime = nowWithOffset if (message.sentTimestamp == null) { @@ -102,9 +104,9 @@ class MessageSender @Inject constructor( // SHARED CONFIG when (destination) { is Destination.Contact -> message.recipient = destination.publicKey - is Destination.LegacyClosedGroup -> message.recipient = destination.groupPublicKey is Destination.ClosedGroup -> message.recipient = destination.publicKey - else -> throw IllegalStateException("Destination should not be an open group.") + is Destination.OpenGroup, + is Destination.OpenGroupInbox -> error("Destination should not be an open group.") } val isSelfSend = (message.recipient == userPublicKey) @@ -140,59 +142,38 @@ class MessageSender @Inject constructor( // Set the timestamp on the content so it can be verified against envelope timestamp proto.setSigTimestampMs(message.sentTimestamp!!) - // Serialize the protobuf - val plaintext = PushTransportDetails.getPaddedMessageBody(proto.build().toByteArray()) - - // Envelope information - val kind: SignalServiceProtos.Envelope.Type - val senderPublicKey: String - when (destination) { + val messageContent = when (destination) { is Destination.Contact -> { - kind = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE - senderPublicKey = "" - } - is Destination.LegacyClosedGroup -> { - kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE - senderPublicKey = destination.groupPublicKey - } - is Destination.ClosedGroup -> { - kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE - senderPublicKey = destination.publicKey + SessionProtocol.encodeFor1o1( + plaintext = proto.build().toByteArray(), + myEd25519PrivKey = userEd25519PrivKey, + timestampMs = message.sentTimestamp!!, + recipientPubKey = Hex.fromStringCondensed(destination.publicKey), + proRotatingEd25519PrivKey = null, + ) } - else -> throw IllegalStateException("Destination should not be open group.") - } - // Encrypt the serialized protobuf - val ciphertext = when (destination) { - is Destination.Contact -> MessageEncrypter.encrypt(plaintext, destination.publicKey) - is Destination.LegacyClosedGroup -> { - val encryptionKeyPair = - MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair( - destination.groupPublicKey - )!! - MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey) - } is Destination.ClosedGroup -> { - val envelope = MessageWrapper.createEnvelope(kind, message.sentTimestamp!!, senderPublicKey, proto.build().toByteArray()) - configFactory.withGroupConfigs(AccountId(destination.publicKey)) { - it.groupKeys.encrypt(envelope.toByteArray()) - } + SessionProtocol.encodeForGroup( + plaintext = proto.build().toByteArray(), + myEd25519PrivKey = userEd25519PrivKey, + timestampMs = message.sentTimestamp!!, + groupEd25519PublicKey = Hex.fromStringCondensed(destination.publicKey), + groupEd25519PrivateKey = configFactory.withGroupConfigs(AccountId(destination.publicKey)) { + it.groupKeys.groupEncKey() + }, + proRotatingEd25519PrivKey = null + ) } - else -> throw IllegalStateException("Destination should not be open group.") - } - // Wrap the result using envelope information - val wrappedMessage = when (destination) { - is Destination.ClosedGroup -> { - // encrypted bytes from the above closed group encryption and envelope steps - ciphertext - } - else -> MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext) + + is Destination.OpenGroup, + is Destination.OpenGroupInbox -> error("Destination should not be an open group.") } - val base64EncodedData = Base64.encodeBytes(wrappedMessage) + // Send the result return SnodeMessage( message.recipient!!, - base64EncodedData, + data = Base64.encodeBytes(messageContent), ttl = getSpecifiedTtl(message, isSyncMessage) ?: message.ttl, messageSendTime ) @@ -207,57 +188,39 @@ class MessageSender @Inject constructor( try { val snodeMessage = buildWrappedMessageToSnode(destination, message, isSyncMessage) - // TODO: this might change in future for config messages - val forkInfo = SnodeAPI.forkInfo - val namespaces: List = when { - destination is Destination.LegacyClosedGroup - && forkInfo.defaultRequiresAuth() -> listOf(Namespace.UNAUTHENTICATED_CLOSED_GROUP()) - - destination is Destination.LegacyClosedGroup - && forkInfo.hasNamespaces() -> listOf( - Namespace.UNAUTHENTICATED_CLOSED_GROUP(), - Namespace.DEFAULT - ()) - destination is Destination.ClosedGroup -> listOf(Namespace.GROUP_MESSAGES()) - - else -> listOf(Namespace.DEFAULT()) - } - - val sendTasks = namespaces.map { namespace -> - if (destination is Destination.ClosedGroup) { - val groupAuth = requireNotNull(configFactory.getGroupAuth(AccountId(destination.publicKey))) { - "Unable to authorize group message send" - } + val sendResult = runCatching { + when (destination) { + is Destination.ClosedGroup -> { + val groupAuth = requireNotNull(configFactory.getGroupAuth(AccountId(destination.publicKey))) { + "Unable to authorize group message send" + } - async { SnodeAPI.sendMessage( auth = groupAuth, message = snodeMessage, - namespace = namespace, + namespace = Namespace.GROUP_MESSAGES(), ) } - } else { - async { - SnodeAPI.sendMessage(snodeMessage, auth = null, namespace = namespace) + is Destination.Contact -> { + SnodeAPI.sendMessage(snodeMessage, auth = null, namespace = Namespace.DEFAULT()) } + is Destination.OpenGroup, + is Destination.OpenGroupInbox -> throw IllegalStateException("Destination should not be an open group.") } } - val sendTaskResults = sendTasks.map { - runCatching { it.await() } - } - - val firstSuccess = sendTaskResults.firstOrNull { it.isSuccess }?.getOrNull() - if (firstSuccess != null) { - message.serverHash = firstSuccess.hash + if (sendResult.isSuccess) { + message.serverHash = sendResult.getOrThrow().hash handleSuccessfulMessageSend(message, destination, isSyncMessage) } else { - // If all tasks failed, throw the first exception - throw sendTaskResults.first().exceptionOrNull()!! + throw sendResult.exceptionOrNull()!! } } catch (exception: Exception) { - handleFailure(exception) + if (exception !is CancellationException) { + handleFailure(exception) + } + throw exception } } @@ -302,7 +265,7 @@ class MessageSender @Inject constructor( val userEdKeyPair = storage.getUserED25519KeyPair()!! var serverCapabilities = listOf() var blindedPublicKey: ByteArray? = null - when(destination) { + when (destination) { is Destination.OpenGroup -> { serverCapabilities = storage.getServerCapabilities(destination.server).orEmpty() storage.getOpenGroupPublicKey(destination.server)?.let { @@ -319,16 +282,9 @@ class MessageSender @Inject constructor( serverPubKey = Hex.fromStringCondensed(destination.serverPublicKey), )?.pubKey?.data } - is Destination.LegacyOpenGroup -> { - serverCapabilities = storage.getServerCapabilities(destination.server).orEmpty() - storage.getOpenGroupPublicKey(destination.server)?.let { - blindedPublicKey = BlindKeyAPI.blind15KeyPairOrNull( - ed25519SecretKey = userEdKeyPair.secretKey.data, - serverPubKey = Hex.fromStringCondensed(it), - )?.pubKey?.data - } - } - else -> {} + + is Destination.ClosedGroup, + is Destination.Contact -> error("Destination must be an open group.") } val messageSender = if (serverCapabilities.contains(Capability.BLIND.name.lowercase()) && blindedPublicKey != null) { AccountId(IdPrefix.BLINDED, blindedPublicKey).hexString @@ -354,8 +310,11 @@ class MessageSender @Inject constructor( if (message !is VisibleMessage || !message.isValid()) { throw Error.InvalidMessage } - val messageBody = content.toByteArray() - val plaintext = PushTransportDetails.getPaddedMessageBody(messageBody) + val plaintext = SessionProtocol.encodeForCommunity( + plaintext = content.toByteArray(), + proRotatingEd25519PrivKey = null + ) + val openGroupMessage = OpenGroupMessage( sender = message.sender, sentTimestamp = message.sentTimestamp!!, @@ -381,13 +340,15 @@ class MessageSender @Inject constructor( if (message !is VisibleMessage || !message.isValid()) { throw Error.InvalidMessage } - val messageBody = content.toByteArray() - val plaintext = PushTransportDetails.getPaddedMessageBody(messageBody) - val ciphertext = MessageEncrypter.encryptBlinded( - plaintext, - destination.blindedPublicKey, - destination.serverPublicKey + val ciphertext = SessionProtocol.encodeForCommunityInbox( + plaintext = content.toByteArray(), + myEd25519PrivKey = userEdKeyPair.secretKey.data, + timestampMs = message.sentTimestamp!!, + recipientPubKey = Hex.fromStringCondensed(destination.blindedPublicKey), + communityServerPubKey = Hex.fromStringCondensed(destination.serverPublicKey), + proRotatingEd25519PrivKey = null, ) + val base64EncodedData = Base64.encodeBytes(ciphertext) val response = OpenGroupApi.sendDirectMessage( base64EncodedData, @@ -408,8 +369,7 @@ class MessageSender @Inject constructor( } // Result Handling - fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) { - val storage = MessagingModuleConfiguration.shared.storage + private fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) { val userPublicKey = storage.getUserPublicKey()!! // Ignore future self-sends storage.addReceivedMessageTimestamp(message.sentTimestamp!!) @@ -430,21 +390,9 @@ class MessageSender @Inject constructor( storage.clearErrorMessage(messageId) // Track the open group server message ID - val messageIsAddressedToCommunity = message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup) + val messageIsAddressedToCommunity = message.openGroupServerMessageID != null && (destination is Destination.OpenGroup) if (messageIsAddressedToCommunity) { - val address = when (destination) { - is Destination.LegacyOpenGroup -> { - Address.Community(destination.server, destination.roomToken) - } - - is Destination.OpenGroup -> { - Address.Community(destination.server, destination.roomToken) - } - - else -> { - throw Exception("Destination was a different destination than we were expecting") - } - } + val address = Address.Community(destination.server, destination.roomToken) val communityThreadID = storage.getThreadId(address) if (communityThreadID != null && communityThreadID >= 0) { storage.setOpenGroupServerMessageID( @@ -498,7 +446,6 @@ class MessageSender @Inject constructor( } // Convenience - @JvmStatic fun send(message: VisibleMessage, address: Address, quote: SignalQuote?, linkPreview: SignalLinkPreview?) { val messageId = message.id if (messageId?.mms == true) { @@ -517,7 +464,6 @@ class MessageSender @Inject constructor( send(message, address) } - @JvmStatic @JvmOverloads fun send(message: Message, address: Address, statusCallback: SendChannel>? = null) { val threadID = storage.getThreadId(address) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 3638d2f493..615bc5ff94 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -1,7 +1,6 @@ package org.session.libsession.messaging.sending_receiving.pollers import com.fasterxml.jackson.core.type.TypeReference -import com.google.protobuf.ByteString import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -42,7 +41,6 @@ import org.session.libsession.messaging.sending_receiving.ReceivedMessageHandler import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.HTTP.Verb.GET @@ -52,7 +50,6 @@ import org.thoughtcrime.securesms.database.BlindMappingRepository import org.thoughtcrime.securesms.database.CommunityDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager -import java.util.concurrent.TimeUnit private typealias PollRequestToken = Channel>> @@ -332,22 +329,16 @@ class OpenGroupPoller @AssistedInject constructor( storage.setLastInboxMessageId(server, lastMessageId) } sortedMessages.forEach { - val encodedMessage = Base64.decode(it.message) - val envelope = SignalServiceProtos.Envelope.newBuilder() - .setTimestampMs(TimeUnit.SECONDS.toMillis(it.postedAt)) - .setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE) - .setContent(ByteString.copyFrom(encodedMessage)) - .setSource(it.sender) - .build() try { - val (message, proto) = messageReceiver.parse( - envelope.toByteArray(), - null, - fromOutbox, - if (fromOutbox) it.recipient else it.sender, - serverPublicKey, - emptySet() // this shouldn't be necessary as we are polling open groups here + val parsed = messageReceiver.parseCommunityInboxMessage( + data = Base64.decode(it.message), + isOutgoing = fromOutbox, + otherBlindedPublicKey = if (fromOutbox) it.recipient else it.sender, + communityServerPubKeyHex = serverPublicKey, ) + + val message = parsed.message + if (fromOutbox) { val syncTarget = blindMappingRepository.getMapping( serverUrl = server, @@ -369,7 +360,7 @@ class OpenGroupPoller @AssistedInject constructor( val threadId = threadDatabase.getThreadIdIfExistsFor(threadAddress) receivedMessageHandler.handle( message = message, - proto = proto, + proto = parsed.proto, threadId = threadId, threadAddress = threadAddress, ) @@ -383,31 +374,24 @@ class OpenGroupPoller @AssistedInject constructor( val threadAddress = Address.Community(serverUrl = server, room = roomToken) // check thread still exists val threadId = storage.getThreadId(threadAddress) ?: return - val envelopes = mutableListOf?>>() - messages.sortedBy { it.serverID!! }.forEach { message -> - if (!message.base64EncodedData.isNullOrEmpty()) { - val envelope = SignalServiceProtos.Envelope.newBuilder() - .setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE) - .setSource(message.sender!!) - .setSourceDevice(1) - .setContent(message.toProto().toByteString()) - .setTimestampMs(message.sentTimestamp) - .build() - envelopes.add(Triple( message.serverID, envelope, message.reactions)) - } - } - envelopes.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { list -> - val parameters = list.map { (serverId, message, reactions) -> - MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId, reactions = reactions) + messages.asSequence() + .map { msg -> + MessageReceiveParameters( + data = Base64.decode(msg.base64EncodedData), + openGroupMessageServerID = msg.serverID, + reactions = msg.reactions, + ) + } + .chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER) + .forEach { params -> + JobQueue.shared.add(batchMessageJobFactory.create( + params, + fromCommunity = threadAddress + )) } - JobQueue.shared.add(batchMessageJobFactory.create( - parameters, - fromCommunity = threadAddress - )) - } - if (envelopes.isNotEmpty()) { + if (messages.isNotEmpty()) { JobQueue.shared.add(trimThreadJobFactory.create(threadId)) } } diff --git a/app/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt b/app/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt index bab6cd0c1a..3fa4a53f15 100644 --- a/app/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt +++ b/app/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt @@ -1,83 +1,14 @@ package org.session.libsession.messaging.utilities -import com.google.protobuf.ByteString import org.session.libsignal.protos.SignalServiceProtos.Envelope import org.session.libsignal.protos.WebSocketProtos.WebSocketMessage -import org.session.libsignal.protos.WebSocketProtos.WebSocketRequestMessage -import org.session.libsignal.utilities.Log -import java.security.SecureRandom object MessageWrapper { - // region Types - sealed class Error(val description: String) : Exception(description) { - object FailedToWrapData : Error("Failed to wrap data.") - object FailedToWrapMessageInEnvelope : Error("Failed to wrap message in envelope.") - object FailedToWrapEnvelopeInWebSocketMessage : Error("Failed to wrap envelope in web socket message.") - object FailedToUnwrapData : Error("Failed to unwrap data.") - } - // endregion - - // region Wrapping - /** - * Wraps `message` in a `SignalServiceProtos.Envelope` and then a `WebSocketProtos.WebSocketMessage` to match the desktop application. - */ - fun wrap(type: Envelope.Type, timestamp: Long, senderPublicKey: String, content: ByteArray): ByteArray { - try { - val envelope = createEnvelope(type, timestamp, senderPublicKey, content) - val webSocketMessage = createWebSocketMessage(envelope) - return webSocketMessage.toByteArray() - } catch (e: Exception) { - throw if (e is Error) e else Error.FailedToWrapData - } - } - - fun createEnvelope(type: Envelope.Type, timestamp: Long, senderPublicKey: String, content: ByteArray): Envelope { - try { - val builder = Envelope.newBuilder() - builder.type = type - builder.timestampMs = timestamp - builder.source = senderPublicKey - builder.sourceDevice = 1 - builder.content = ByteString.copyFrom(content) - return builder.build() - } catch (e: Exception) { - Log.d("Loki", "Failed to wrap message in envelope: ${e.message}.") - throw Error.FailedToWrapMessageInEnvelope - } - } - - private fun createWebSocketMessage(envelope: Envelope): WebSocketMessage { - try { - return WebSocketMessage.newBuilder().apply { - request = WebSocketRequestMessage.newBuilder().apply { - verb = "PUT" - path = "/api/v1/message" - id = SecureRandom().nextLong() - body = envelope.toByteString() - }.build() - type = WebSocketMessage.Type.REQUEST - }.build() - } catch (e: Exception) { - Log.d("MessageWrapper", "Failed to wrap envelope in web socket message: ${e.message}.") - throw Error.FailedToWrapEnvelopeInWebSocketMessage - } - } - // endregion - - // region Unwrapping - /** - * `data` shouldn't be base 64 encoded. - */ fun unwrap(data: ByteArray): Envelope { - try { - val webSocketMessage = WebSocketMessage.parseFrom(data) - val envelopeAsData = webSocketMessage.request.body - return Envelope.parseFrom(envelopeAsData) - } catch (e: Exception) { - Log.d("MessageWrapper", "Failed to unwrap data", e) - throw Error.FailedToUnwrapData - } + val webSocketMessage = WebSocketMessage.parseFrom(data) + val envelopeAsData = webSocketMessage.request.body + return Envelope.parseFrom(envelopeAsData) } // endregion } diff --git a/app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java b/app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java deleted file mode 100644 index bdd9964d5e..0000000000 --- a/app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -package org.session.libsignal.crypto; - -import org.session.libsignal.utilities.Log; - -public class PushTransportDetails { - - private static final String TAG = PushTransportDetails.class.getSimpleName(); - - public static byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) { - int paddingStart = 0; - - for (int i=messageWithPadding.length-1;i>=0;i--) { - if (messageWithPadding[i] == (byte)0x80) { - paddingStart = i; - break; - } else if (messageWithPadding[i] != (byte)0x00) { - Log.w(TAG, "Padding byte is malformed, returning unstripped padding."); - return messageWithPadding; - } - } - - byte[] strippedMessage = new byte[paddingStart]; - System.arraycopy(messageWithPadding, 0, strippedMessage, 0, strippedMessage.length); - - return strippedMessage; - } - - public static byte[] getPaddedMessageBody(byte[] messageBody) { - // NOTE: This is dumb. We have our own padding scheme, but so does the cipher. - // The +1 -1 here is to make sure the Cipher has room to add one padding byte, - // otherwise it'll add a full 16 extra bytes. - byte[] paddedMessage = new byte[getPaddedMessageLength(messageBody.length + 1) - 1]; - System.arraycopy(messageBody, 0, paddedMessage, 0, messageBody.length); - paddedMessage[messageBody.length] = (byte)0x80; - - return paddedMessage; - } - - private static int getPaddedMessageLength(int messageLength) { - int messageLengthWithTerminator = messageLength + 1; - int messagePartCount = messageLengthWithTerminator / 160; - - if (messageLengthWithTerminator % 160 != 0) { - messagePartCount++; - } - - return messagePartCount * 160; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 564a2a8234..2437bfca0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -705,16 +705,6 @@ open class Storage @Inject constructor( return lokiAPIDatabase.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) } - override fun getAllLegacyGroupPublicKeys(): Set { - return lokiAPIDatabase.getAllClosedGroupPublicKeys() - } - - override fun getAllActiveClosedGroupPublicKeys(): Set { - return lokiAPIDatabase.getAllClosedGroupPublicKeys().filter { - getGroup(GroupUtil.doubleEncodeGroupID(it))?.isActive == true - }.toSet() - } - override fun addClosedGroupPublicKey(groupPublicKey: String) { lokiAPIDatabase.addClosedGroupPublicKey(groupPublicKey) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f632e491ef..81df3b7393 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ kotlinVersion = "2.2.20" kryoVersion = "5.6.2" kspVersion = "2.3.0" legacySupportV13Version = "1.0.0" -libsessionUtilAndroidVersion = "1.0.9-2-g8c03d1e" +libsessionUtilAndroidVersion = "1.0.9-14-g80f501d" media3ExoplayerVersion = "1.8.0" mockitoCoreVersion = "5.20.0" navVersion = "2.9.5" From aa7cd901b7e28968ddf50fbb3734b1b50d15d9c6 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:22:40 +1100 Subject: [PATCH 05/19] Reverted changes to batch receiver --- .../libsession/database/StorageProtocol.kt | 2 + .../messaging/jobs/BatchMessageReceiveJob.kt | 73 ++--- .../messages/visible/ParsedMessage.kt | 2 +- .../sending_receiving/MessageHandler.kt | 250 ++++++++++++++++++ .../sending_receiving/MessageReceiver.kt | 214 +++++++-------- .../crypto/PushTransportDetails.java | 55 ++++ .../securesms/database/Storage.kt | 10 + 7 files changed, 433 insertions(+), 173 deletions(-) create mode 100644 app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt create mode 100644 app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java diff --git a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt index f77b398165..f30b4aef98 100644 --- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -113,6 +113,8 @@ interface StorageProtocol { fun setActive(groupID: String, value: Boolean) fun removeMember(groupID: String, member: Address) fun updateMembers(groupID: String, members: List
) + fun getAllLegacyGroupPublicKeys(): Set + fun getAllActiveClosedGroupPublicKeys(): Set fun addClosedGroupPublicKey(groupPublicKey: String) fun removeClosedGroupPublicKey(groupPublicKey: String) fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index b6581fb2ab..ced99a7f2a 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -36,7 +36,6 @@ import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.UserConfigType import org.session.libsignal.protos.UtilProtos -import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.ThreadDatabase @@ -44,7 +43,7 @@ import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.ReactionRecord import kotlin.math.max -class MessageReceiveParameters( +data class MessageReceiveParameters( val data: ByteArray, val serverHash: String? = null, val openGroupMessageServerID: Long? = null, @@ -153,49 +152,29 @@ class BatchMessageReceiveJob @AssistedInject constructor( suspend fun executeAsync(dispatcherName: String) { val threadMap = mutableMapOf>>() val localUserPublicKey = storage.getUserPublicKey() + val serverPublicKey = fromCommunity?.let { storage.getOpenGroupPublicKey(it.serverUrl) } + val currentClosedGroups = storage.getAllActiveClosedGroupPublicKeys() // parse and collect IDs - messages.forEach { params -> + messages.forEach { messageParameters -> + val (data, serverHash, openGroupMessageServerID) = messageParameters try { - val parsedParams = when { - fromCommunity != null -> { - messageReceiver.parseCommunityMessage( - data = params.data, - messageServerId = requireNotNull(params.openGroupMessageServerID) { - "Open group message must have server ID" - }, - communityServerPubKeyHex = requireNotNull(storage.getOpenGroupPublicKey(fromCommunity.serverUrl)) { - "Given message doesn't have a valid community server public key or the group has been deleted" - } - ) - } - - params.closedGroup != null -> { - messageReceiver.parseGroupMessage( - data = params.data, - serverHash = requireNotNull(params.serverHash) { - "Closed group message must have server hash" - }, - groupId = AccountId(params.closedGroup.publicKey) - ) - } - - else -> { - messageReceiver.parse1o1Message( - data = params.data, - serverHash = requireNotNull(params.serverHash) { - "1on1 message must have server hash" - } - ) - } - } + val (message, proto) = messageReceiver.parse( + data, + openGroupMessageServerID, + openGroupPublicKey = serverPublicKey, + currentClosedGroups = currentClosedGroups, + closedGroupSessionId = messageParameters.closedGroup?.publicKey + ) + message.serverHash = serverHash + val parsedParams = ParsedMessage(messageParameters, message, proto) - if(isHidden(parsedParams.message)) return@forEach + if(isHidden(message)) return@forEach val threadAddress = when { fromCommunity != null -> fromCommunity - parsedParams.message.groupPublicKey != null -> parsedParams.message.groupPublicKey!!.toAddress() - else -> parsedParams.message.senderOrSync.toAddress() + message.groupPublicKey != null -> message.groupPublicKey!!.toAddress() + else -> message.senderOrSync.toAddress() } as Address.Conversable val threadID = if (shouldCreateThread(parsedParams)) { @@ -215,12 +194,12 @@ class BatchMessageReceiveJob @AssistedInject constructor( } else { Log.e(TAG, "Couldn't receive message, failed (id: $id)", e) - failures += params + failures += messageParameters } } else -> { Log.e(TAG, "Couldn't receive message, failed (id: $id)", e) - failures += params + failures += messageParameters } } } @@ -239,9 +218,9 @@ class BatchMessageReceiveJob @AssistedInject constructor( val communityReactions = mutableMapOf>() - messages.forEach { msg -> + messages.forEach { (parameters, message, proto) -> try { - when (val message = msg.message) { + when (message) { is VisibleMessage -> { val isUserBlindedSender = message.sender == handlerContext.userBlindedKey @@ -252,7 +231,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( } val messageId = receivedMessageHandler.handleVisibleMessage( message = message, - proto = msg.proto, + proto = proto, context = handlerContext, runThreadUpdate = false, runProfileUpdate = true @@ -265,11 +244,11 @@ class BatchMessageReceiveJob @AssistedInject constructor( ) } - msg.parameters.openGroupMessageServerID?.let { + parameters.openGroupMessageServerID?.let { constructReactionRecords( openGroupMessageServerID = it, context = handlerContext, - reactions = msg.parameters.reactions, + reactions = parameters.reactions, out = communityReactions ) } @@ -286,7 +265,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( else -> receivedMessageHandler.handle( message = message, - proto = msg.proto, + proto = proto, threadId = threadId, threadAddress = threadAddress ) @@ -297,7 +276,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( Log.e(TAG, "Message failed permanently (id: $id)", e) } else { Log.e(TAG, "Message failed (id: $id)", e) - failures += msg.parameters + failures += parameters } } } diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt index 5f0ecdd2a7..b0c5ced904 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt +++ b/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt @@ -4,7 +4,7 @@ import org.session.libsession.messaging.jobs.MessageReceiveParameters import org.session.libsession.messaging.messages.Message import org.session.libsignal.protos.SignalServiceProtos -class ParsedMessage( +data class ParsedMessage( val parameters: MessageReceiveParameters, val message: Message, val proto: SignalServiceProtos.Content diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt new file mode 100644 index 0000000000..2628727266 --- /dev/null +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt @@ -0,0 +1,250 @@ +package org.session.libsession.messaging.sending_receiving + +import network.loki.messenger.libsession_util.protocol.DecodedEnvelop +import network.loki.messenger.libsession_util.protocol.SessionProtocol +import network.loki.messenger.libsession_util.util.BlindKeyAPI +import org.session.libsession.database.StorageProtocol +import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.control.CallMessage +import org.session.libsession.messaging.messages.control.DataExtractionNotification +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.GroupUpdated +import org.session.libsession.messaging.messages.control.MessageRequestResponse +import org.session.libsession.messaging.messages.control.ReadReceipt +import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsession.messaging.messages.control.UnsendRequest +import org.session.libsession.messaging.messages.visible.ParsedMessage +import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.open_groups.OpenGroupMessage +import org.session.libsession.snode.SnodeClock +import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsignal.exceptions.NonRetryableException +import org.session.libsignal.protos.SignalServiceProtos +import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.IdPrefix +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.math.abs + +@Singleton +class MessageHandler @Inject constructor( + private val configFactory: ConfigFactoryProtocol, + private val storage: StorageProtocol, + private val snodeClock: SnodeClock, +) { + + //TODO: Obtain proBackendKey from somewhere + private val proBackendKey = ByteArray(32) + + // A faster way to check if the user is blocked than to go through RecipientRepository + private fun isUserBlocked(accountId: AccountId): Boolean { + return configFactory.withUserConfigs { it.contacts.get(accountId.hexString) } + ?.blocked == true + } + + + private fun createMessageFromProto(proto: SignalServiceProtos.Content, isGroupMessage: Boolean): Message { + val message = ReadReceipt.fromProto(proto) ?: + TypingIndicator.fromProto(proto) ?: + DataExtractionNotification.fromProto(proto) ?: + ExpirationTimerUpdate.fromProto(proto, isGroupMessage) ?: + UnsendRequest.fromProto(proto) ?: + MessageRequestResponse.fromProto(proto) ?: + CallMessage.fromProto(proto) ?: + GroupUpdated.fromProto(proto) ?: + VisibleMessage.fromProto(proto) + + if (message == null) { + throw NonRetryableException("Unknown message type") + } + + return message + } + + private fun parseMessage( + decodedEnvelop: DecodedEnvelop, + relaxSignatureCheck: Boolean, + checkForBlockStatus: Boolean, + isForGroup: Boolean, + currentUserId: AccountId, + alternativeCurrentUserIds: List, + senderIdPrefix: IdPrefix + ): Pair { + return parseMessage( + sender = AccountId(senderIdPrefix, decodedEnvelop.senderX25519PubKey.data), + contentPlaintext = decodedEnvelop.contentPlainText.data, + messageTimestampMs = decodedEnvelop.timestamp.toEpochMilli(), + relaxSignatureCheck = relaxSignatureCheck, + checkForBlockStatus = checkForBlockStatus, + isForGroup = isForGroup, + currentUserId = currentUserId, + alternativeCurrentUserIds = alternativeCurrentUserIds, + ) + } + + private fun parseMessage( + sender: AccountId, + contentPlaintext: ByteArray, + messageTimestampMs: Long, + relaxSignatureCheck: Boolean, + checkForBlockStatus: Boolean, + isForGroup: Boolean, + currentUserId: AccountId, + alternativeCurrentUserIds: List, + ): Pair { + val proto = SignalServiceProtos.Content.parseFrom(contentPlaintext) + + // Check signature + if (proto.hasSigTimestampMs()) { + val diff = abs(proto.sigTimestampMs - messageTimestampMs) + if ( + (!relaxSignatureCheck && diff != 0L ) || + (relaxSignatureCheck && diff > TimeUnit.HOURS.toMillis(6))) { + throw NonRetryableException("Invalid signature timestamp") + } + } + + val message = createMessageFromProto(proto, isGroupMessage = isForGroup) + + // Blocked sender check + if (checkForBlockStatus && isUserBlocked(sender) && message.shouldDiscardIfBlocked()) { + throw NonRetryableException("Sender($sender) is blocked from sending message to us") + } + + // Valid self-send messages + val isSenderSelf = sender == currentUserId || sender in alternativeCurrentUserIds + if (isSenderSelf && !message.isSelfSendValid) { + throw NonRetryableException("Ignoring self send message") + } + + // Fill in message fields + message.sender = sender.hexString + message.recipient = currentUserId.hexString + message.sentTimestamp = messageTimestampMs + message.receivedTimestamp = snodeClock.currentTimeMills() + message.isSenderSelf = isSenderSelf + + // Validate + var isValid = message.isValid() + // TODO: Legacy code: why this is check needed? + if (message is VisibleMessage && !isValid && proto.dataMessage.attachmentsCount != 0) { isValid = true } + if (!isValid) { + throw NonRetryableException("Invalid message") + } + + // Duplicate check + // TODO: Legacy code: find out why is this needed again (it was done using server hash when receiving messages) + if (storage.isDuplicateMessage(messageTimestampMs)) { + throw NonRetryableException("Duplicate message") + } + storage.addReceivedMessageTimestamp(messageTimestampMs) + + return message to proto + } + + + fun parse1o1Message( + data: ByteArray, + serverHash: String, + ): Pair { + val envelop = SessionProtocol.decodeFor1o1( + myEd25519PrivKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.data) { + "Couldn't find current user's key" + }, + payload = data, + nowEpochMs = snodeClock.currentTimeMills(), + proBackendPubKey = proBackendKey, + ) + + return parseMessage( + decodedEnvelop = envelop, + relaxSignatureCheck = false, + checkForBlockStatus = true, + isForGroup = false, + senderIdPrefix = IdPrefix.STANDARD, + currentUserId = AccountId(storage.getUserPublicKey()!!), + alternativeCurrentUserIds = emptyList(), + ).also { (message, _) -> + message.serverHash = serverHash + } + } + + fun parseGroupMessage( + data: ByteArray, + serverHash: String, + groupId: AccountId, + ): Pair { + val keys = configFactory.withGroupConfigs(groupId) { + it.groupKeys.groupKeys() + } + + val decoded = SessionProtocol.decodeForGroup( + payload = data, + myEd25519PrivKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.data) { + "Couldn't find current user's key" + }, + nowEpochMs = snodeClock.currentTimeMills(), + groupEd25519PublicKey = groupId.pubKeyBytes, + groupEd25519PrivateKeys = keys.toTypedArray(), + proBackendPubKey = proBackendKey + ) + + return parseMessage( + decodedEnvelop = decoded, + relaxSignatureCheck = false, + checkForBlockStatus = false, + isForGroup = true, + senderIdPrefix = IdPrefix.STANDARD, + currentUserId = AccountId(storage.getUserPublicKey()!!), + alternativeCurrentUserIds = emptyList(), + ).also { (message, _) -> + message.serverHash = serverHash + } + } + + fun parseCommunityMessage( + msg: OpenGroupMessage, + communityServerPubKeyHex: String, + ): Pair { + val decoded = SessionProtocol.decodeForCommunity( + payload = Base64.decode(msg.base64EncodedData.orEmpty()), + nowEpochMs = snodeClock.currentTimeMills(), + proBackendPubKey = proBackendKey, + ) + + val sender = AccountId(msg.sender!!) + + val keyPair = requireNotNull(storage.getUserED25519KeyPair()) { + "Couldn't find current user's key" + } + + val currentUserId = AccountId(IdPrefix.STANDARD, keyPair.pubKey.data) + + return parseMessage( + contentPlaintext = decoded.contentPlainText.data, + relaxSignatureCheck = true, + checkForBlockStatus = false, + isForGroup = false, + currentUserId = currentUserId, + sender = sender, + messageTimestampMs = msg.sentTimestamp, + alternativeCurrentUserIds = BlindKeyAPI.blind15Ids( + sessionId = currentUserId.hexString, + serverPubKey = communityServerPubKeyHex, + ).map(::AccountId), + ).also { (message, _) -> + message.openGroupServerMessageID = msg.serverID + } + } + + fun parseCommunityInboxMessage( + data: ByteArray, + isOutgoing: Boolean, + otherBlindedPublicKey: String, + communityServerPubKeyHex: String, + ): ParsedMessage { + TODO() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index af58ba9719..a5307b8757 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -1,6 +1,5 @@ package org.session.libsession.messaging.sending_receiving -import network.loki.messenger.libsession_util.protocol.SessionProtocol import network.loki.messenger.libsession_util.util.BlindKeyAPI import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.messages.Message @@ -12,11 +11,10 @@ import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.ReadReceipt import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.UnsendRequest -import org.session.libsession.messaging.messages.visible.ParsedMessage import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsignal.crypto.PushTransportDetails import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos.Envelope import org.session.libsignal.utilities.AccountId @@ -32,7 +30,6 @@ import kotlin.math.abs class MessageReceiver @Inject constructor( private val configFactory: ConfigFactoryProtocol, private val storage: StorageProtocol, - private val snodeClock: SnodeClock, ) { internal sealed class Error(message: String) : Exception(message) { @@ -61,46 +58,7 @@ class MessageReceiver @Inject constructor( } } - fun parse1o1Message( - data: ByteArray, - serverHash: String, - ): ParsedMessage { - - SessionProtocol.decodeEnvelope() - TODO() - - message.serverHash = serverHash - } - - fun parseGroupMessage( - data: ByteArray, - serverHash: String, - groupId: AccountId, - ): ParsedMessage { - TODO() - - message.serverHash = serverHash - } - - fun parseCommunityMessage( - data: ByteArray, - messageServerId: Long, - communityServerPubKeyHex: String, - ): ParsedMessage { - TODO() - } - - fun parseCommunityInboxMessage( - data: ByteArray, - isOutgoing: Boolean, - otherBlindedPublicKey: String, - communityServerPubKeyHex: String, - ): ParsedMessage { - TODO() - } - - private fun parse( - proto: SignalServiceProtos.Content, + internal fun parse( data: ByteArray, openGroupServerID: Long?, isOutgoing: Boolean? = null, @@ -109,85 +67,86 @@ class MessageReceiver @Inject constructor( currentClosedGroups: Set?, closedGroupSessionId: String? = null, ): Pair { -// val userPublicKey = storage.getUserPublicKey() -// val isOpenGroupMessage = (openGroupServerID != null) -// var plaintext: ByteArray? = null -// var sender: String? = null -// var groupPublicKey: String? = null -// // Parse the envelope -// val envelope = Envelope.parseFrom(data) ?: throw Error.InvalidMessage -// // Decrypt the contents -// val envelopeContent = envelope.content ?: run { -// throw Error.NoData -// } -// -// if (isOpenGroupMessage) { -// plaintext = envelopeContent.toByteArray() -// sender = envelope.source -// } else { -// when (envelope.type) { -// SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> { -// if (IdPrefix.fromValue(envelope.source)?.isBlinded() == true) { -// openGroupPublicKey ?: throw Error.InvalidGroupPublicKey -// otherBlindedPublicKey ?: throw Error.DecryptionFailed -// val decryptionResult = MessageDecrypter.decryptBlinded( -// envelopeContent.toByteArray(), -// isOutgoing ?: false, -// otherBlindedPublicKey, -// openGroupPublicKey -// ) -// plaintext = decryptionResult.first -// sender = decryptionResult.second -// } else { -// val userX25519KeyPair = storage.getUserX25519KeyPair() -// val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), userX25519KeyPair) -// plaintext = decryptionResult.first -// sender = decryptionResult.second -// } -// } -// SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE -> { -// val hexEncodedGroupPublicKey = closedGroupSessionId ?: envelope.source -// val sessionId = AccountId(hexEncodedGroupPublicKey) -// if (sessionId.prefix == IdPrefix.GROUP) { -// plaintext = envelopeContent.toByteArray() -// sender = envelope.source -// groupPublicKey = hexEncodedGroupPublicKey -// } else { -// if (!storage.isLegacyClosedGroup(hexEncodedGroupPublicKey)) { -// throw Error.InvalidGroupPublicKey -// } -// val encryptionKeyPairs = storage.getClosedGroupEncryptionKeyPairs(hexEncodedGroupPublicKey) -// if (encryptionKeyPairs.isEmpty()) { -// throw Error.NoGroupKeyPair -// } -// // Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than -// // likely be the one we want) but try older ones in case that didn't work) -// var encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex) -// fun decrypt() { -// try { -// val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), encryptionKeyPair) -// plaintext = decryptionResult.first -// sender = decryptionResult.second -// } catch (e: Exception) { -// if (encryptionKeyPairs.isNotEmpty()) { -// encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex) -// decrypt() -// } else { -// Log.e("Loki", "Failed to decrypt group message", e) -// throw e -// } -// } -// } -// groupPublicKey = hexEncodedGroupPublicKey -// decrypt() -// } -// } -// else -> { -// throw Error.UnknownEnvelopeType -// } -// } -// } -// // Parse the proto + val userPublicKey = storage.getUserPublicKey() + val isOpenGroupMessage = (openGroupServerID != null) + var plaintext: ByteArray? = null + var sender: String? = null + var groupPublicKey: String? = null + // Parse the envelope + val envelope = Envelope.parseFrom(data) ?: throw Error.InvalidMessage + // Decrypt the contents + val envelopeContent = envelope.content ?: run { + throw Error.NoData + } + + if (isOpenGroupMessage) { + plaintext = envelopeContent.toByteArray() + sender = envelope.source + } else { + when (envelope.type) { + SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> { + if (IdPrefix.fromValue(envelope.source)?.isBlinded() == true) { + openGroupPublicKey ?: throw Error.InvalidGroupPublicKey + otherBlindedPublicKey ?: throw Error.DecryptionFailed + val decryptionResult = MessageDecrypter.decryptBlinded( + envelopeContent.toByteArray(), + isOutgoing ?: false, + otherBlindedPublicKey, + openGroupPublicKey + ) + plaintext = decryptionResult.first + sender = decryptionResult.second + } else { + val userX25519KeyPair = storage.getUserX25519KeyPair() + val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), userX25519KeyPair) + plaintext = decryptionResult.first + sender = decryptionResult.second + } + } + SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE -> { + val hexEncodedGroupPublicKey = closedGroupSessionId ?: envelope.source + val sessionId = AccountId(hexEncodedGroupPublicKey) + if (sessionId.prefix == IdPrefix.GROUP) { + plaintext = envelopeContent.toByteArray() + sender = envelope.source + groupPublicKey = hexEncodedGroupPublicKey + } else { + if (!storage.isLegacyClosedGroup(hexEncodedGroupPublicKey)) { + throw Error.InvalidGroupPublicKey + } + val encryptionKeyPairs = storage.getClosedGroupEncryptionKeyPairs(hexEncodedGroupPublicKey) + if (encryptionKeyPairs.isEmpty()) { + throw Error.NoGroupKeyPair + } + // Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than + // likely be the one we want) but try older ones in case that didn't work) + var encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex) + fun decrypt() { + try { + val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), encryptionKeyPair) + plaintext = decryptionResult.first + sender = decryptionResult.second + } catch (e: Exception) { + if (encryptionKeyPairs.isNotEmpty()) { + encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex) + decrypt() + } else { + Log.e("Loki", "Failed to decrypt group message", e) + throw e + } + } + } + groupPublicKey = hexEncodedGroupPublicKey + decrypt() + } + } + else -> { + throw Error.UnknownEnvelopeType + } + } + } + // Parse the proto + val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext)) // Verify the signature timestamp inside the content is the same as in envelope. // If the message is from an open group, 6 hours of difference is allowed. @@ -238,7 +197,7 @@ class MessageReceiver @Inject constructor( message.sender = sender message.recipient = userPublicKey message.sentTimestamp = envelope.timestampMs - message.receivedTimestamp = if (envelope.hasServerTimestampMs()) envelope.serverTimestampMs else snodeClock.currentTimeMills() + message.receivedTimestamp = if (envelope.hasServerTimestampMs()) envelope.serverTimestampMs else SnodeAPI.nowWithOffset message.groupPublicKey = groupPublicKey message.openGroupServerMessageID = openGroupServerID // Validate @@ -247,7 +206,12 @@ class MessageReceiver @Inject constructor( if (!isValid) { throw Error.InvalidMessage } - + // If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp + // will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround + // for this issue. + if (groupPublicKey != null && groupPublicKey !in (currentClosedGroups ?: emptySet()) && IdPrefix.fromValue(groupPublicKey) != IdPrefix.GROUP) { + throw Error.NoGroupThread + } if (storage.isDuplicateMessage(envelope.timestampMs)) { throw Error.DuplicateMessage } storage.addReceivedMessageTimestamp(envelope.timestampMs) // Return diff --git a/app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java b/app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java new file mode 100644 index 0000000000..bdd9964d5e --- /dev/null +++ b/app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2014-2016 Open Whisper Systems + * + * Licensed according to the LICENSE file in this repository. + */ + +package org.session.libsignal.crypto; + +import org.session.libsignal.utilities.Log; + +public class PushTransportDetails { + + private static final String TAG = PushTransportDetails.class.getSimpleName(); + + public static byte[] getStrippedPaddingMessageBody(byte[] messageWithPadding) { + int paddingStart = 0; + + for (int i=messageWithPadding.length-1;i>=0;i--) { + if (messageWithPadding[i] == (byte)0x80) { + paddingStart = i; + break; + } else if (messageWithPadding[i] != (byte)0x00) { + Log.w(TAG, "Padding byte is malformed, returning unstripped padding."); + return messageWithPadding; + } + } + + byte[] strippedMessage = new byte[paddingStart]; + System.arraycopy(messageWithPadding, 0, strippedMessage, 0, strippedMessage.length); + + return strippedMessage; + } + + public static byte[] getPaddedMessageBody(byte[] messageBody) { + // NOTE: This is dumb. We have our own padding scheme, but so does the cipher. + // The +1 -1 here is to make sure the Cipher has room to add one padding byte, + // otherwise it'll add a full 16 extra bytes. + byte[] paddedMessage = new byte[getPaddedMessageLength(messageBody.length + 1) - 1]; + System.arraycopy(messageBody, 0, paddedMessage, 0, messageBody.length); + paddedMessage[messageBody.length] = (byte)0x80; + + return paddedMessage; + } + + private static int getPaddedMessageLength(int messageLength) { + int messageLengthWithTerminator = messageLength + 1; + int messagePartCount = messageLengthWithTerminator / 160; + + if (messageLengthWithTerminator % 160 != 0) { + messagePartCount++; + } + + return messagePartCount * 160; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 2437bfca0f..564a2a8234 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -705,6 +705,16 @@ open class Storage @Inject constructor( return lokiAPIDatabase.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) } + override fun getAllLegacyGroupPublicKeys(): Set { + return lokiAPIDatabase.getAllClosedGroupPublicKeys() + } + + override fun getAllActiveClosedGroupPublicKeys(): Set { + return lokiAPIDatabase.getAllClosedGroupPublicKeys().filter { + getGroup(GroupUtil.doubleEncodeGroupID(it))?.isActive == true + }.toSet() + } + override fun addClosedGroupPublicKey(groupPublicKey: String) { lokiAPIDatabase.addClosedGroupPublicKey(groupPublicKey) } From 7cc59c55aeb7920e9458590066e2ba100439e045 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:25:17 +1100 Subject: [PATCH 06/19] Deprecate classes --- .../libsession/messaging/jobs/BatchMessageReceiveJob.kt | 3 ++- .../libsession/messaging/sending_receiving/MessageReceiver.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index ced99a7f2a..8a12a15ea1 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -51,6 +51,7 @@ data class MessageReceiveParameters( val closedGroup: Destination.ClosedGroup? = null ) +@Deprecated("BatchMessageReceiveJob is now only here so that existing persisted jobs can be processed.") class BatchMessageReceiveJob @AssistedInject constructor( @Assisted private val messages: List, @Assisted val fromCommunity: Address.Community?, // The community the messages are received in, if any @@ -360,7 +361,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( @AssistedFactory abstract class Factory : Job.DeserializeFactory { - abstract fun create( + protected abstract fun create( messages: List, fromCommunity: Address.Community?, ): BatchMessageReceiveJob diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index a5307b8757..3e0946027d 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -26,9 +26,9 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.math.abs +@Deprecated("This class only exists so the old BatchMessageReceiver can function. New code should use MessageHandler directly.") @Singleton class MessageReceiver @Inject constructor( - private val configFactory: ConfigFactoryProtocol, private val storage: StorageProtocol, ) { From abbea740c6662ed0b016f7b6609750071afd2ae9 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:27:27 +1100 Subject: [PATCH 07/19] Deprecate classes --- .../messaging/sending_receiving/MessageDecrypter.kt | 1 + .../messaging/sending_receiving/MessageEncrypter.kt | 1 + .../org/session/libsignal/crypto/PushTransportDetails.java | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt index 4ee0cd8bc1..6418ac99bd 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt @@ -10,6 +10,7 @@ import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.hexEncodedPublicKey import org.session.libsignal.utilities.removingIdPrefixIfNeeded +@Deprecated("This class is deprecated and new code should try to decrypt/decode message using SessionProtocol API") object MessageDecrypter { /** diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt index 17f16ddbfe..cc09b12a37 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt @@ -8,6 +8,7 @@ import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.removingIdPrefixIfNeeded +@Deprecated("This class is deprecated and new code should try to encrypt/encode message using SessionProtocol API") object MessageEncrypter { /** diff --git a/app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java b/app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java index bdd9964d5e..536b1e13a4 100644 --- a/app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java +++ b/app/src/main/java/org/session/libsignal/crypto/PushTransportDetails.java @@ -8,6 +8,11 @@ import org.session.libsignal.utilities.Log; +/** + * @deprecated The logic here has been moved to SessionProtocol, this class only exists + * so the old persisted message queue can be read. It will be removed in a future release. + */ +@Deprecated(forRemoval = true) public class PushTransportDetails { private static final String TAG = PushTransportDetails.class.getSimpleName(); From 30d38c48a7a5f4fddf33c0a72363279895373cc0 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:11:18 +1100 Subject: [PATCH 08/19] Added new ReceivedMessageHashDatabase to store hashes --- .../messaging/jobs/BatchMessageReceiveJob.kt | 3 +- .../messaging/open_groups/OpenGroupMessage.kt | 7 + .../pollers/OpenGroupPoller.kt | 64 +++++---- .../sending_receiving/pollers/Poller.kt | 14 +- .../org/session/libsession/snode/SnodeAPI.kt | 37 ----- .../database/LokiAPIDatabaseProtocol.kt | 5 - .../components/TypingStatusSender.java | 6 +- .../securesms/configs/ConfigToDatabaseSync.kt | 4 +- .../securesms/database/LokiAPIDatabase.kt | 40 +----- .../database/ReceivedMessageHashDatabase.kt | 126 ++++++++++++++++++ .../securesms/database/ThreadDatabase.java | 2 +- .../database/helpers/SQLCipherOpenHelper.java | 10 +- .../securesms/groups/GroupManagerV2Impl.kt | 4 +- .../securesms/groups/GroupPoller.kt | 7 +- .../AndroidAutoReplyReceiver.java | 5 +- .../notifications/RemoteReplyReceiver.java | 6 +- .../manager/CreateAccountManager.kt | 6 +- .../onboarding/manager/LoadAccountManager.kt | 6 +- 18 files changed, 225 insertions(+), 127 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index 8a12a15ea1..889f0709bf 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -361,7 +361,8 @@ class BatchMessageReceiveJob @AssistedInject constructor( @AssistedFactory abstract class Factory : Job.DeserializeFactory { - protected abstract fun create( + @Deprecated("New code should try to handle message directly instead of creating this job") + abstract fun create( messages: List, fromCommunity: Address.Community?, ): BatchMessageReceiveJob diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt index 5b493a1b42..625061bf0e 100644 --- a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt +++ b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt @@ -4,6 +4,8 @@ import network.loki.messenger.libsession_util.ED25519 import network.loki.messenger.libsession_util.util.BlindKeyAPI import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability +import org.session.libsignal.crypto.PushTransportDetails +import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64.decode import org.session.libsignal.utilities.Log @@ -82,4 +84,9 @@ data class OpenGroupMessage( base64EncodedSignature?.let { json["signature"] = it } return json } + + fun toProto(): SignalServiceProtos.Content { + val data = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody) + return SignalServiceProtos.Content.parseFrom(data) + } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 615bc5ff94..3638d2f493 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -1,6 +1,7 @@ package org.session.libsession.messaging.sending_receiving.pollers import com.fasterxml.jackson.core.type.TypeReference +import com.google.protobuf.ByteString import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -41,6 +42,7 @@ import org.session.libsession.messaging.sending_receiving.ReceivedMessageHandler import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.HTTP.Verb.GET @@ -50,6 +52,7 @@ import org.thoughtcrime.securesms.database.BlindMappingRepository import org.thoughtcrime.securesms.database.CommunityDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager +import java.util.concurrent.TimeUnit private typealias PollRequestToken = Channel>> @@ -329,16 +332,22 @@ class OpenGroupPoller @AssistedInject constructor( storage.setLastInboxMessageId(server, lastMessageId) } sortedMessages.forEach { + val encodedMessage = Base64.decode(it.message) + val envelope = SignalServiceProtos.Envelope.newBuilder() + .setTimestampMs(TimeUnit.SECONDS.toMillis(it.postedAt)) + .setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE) + .setContent(ByteString.copyFrom(encodedMessage)) + .setSource(it.sender) + .build() try { - val parsed = messageReceiver.parseCommunityInboxMessage( - data = Base64.decode(it.message), - isOutgoing = fromOutbox, - otherBlindedPublicKey = if (fromOutbox) it.recipient else it.sender, - communityServerPubKeyHex = serverPublicKey, + val (message, proto) = messageReceiver.parse( + envelope.toByteArray(), + null, + fromOutbox, + if (fromOutbox) it.recipient else it.sender, + serverPublicKey, + emptySet() // this shouldn't be necessary as we are polling open groups here ) - - val message = parsed.message - if (fromOutbox) { val syncTarget = blindMappingRepository.getMapping( serverUrl = server, @@ -360,7 +369,7 @@ class OpenGroupPoller @AssistedInject constructor( val threadId = threadDatabase.getThreadIdIfExistsFor(threadAddress) receivedMessageHandler.handle( message = message, - proto = parsed.proto, + proto = proto, threadId = threadId, threadAddress = threadAddress, ) @@ -374,24 +383,31 @@ class OpenGroupPoller @AssistedInject constructor( val threadAddress = Address.Community(serverUrl = server, room = roomToken) // check thread still exists val threadId = storage.getThreadId(threadAddress) ?: return - - messages.asSequence() - .map { msg -> - MessageReceiveParameters( - data = Base64.decode(msg.base64EncodedData), - openGroupMessageServerID = msg.serverID, - reactions = msg.reactions, - ) + val envelopes = mutableListOf?>>() + messages.sortedBy { it.serverID!! }.forEach { message -> + if (!message.base64EncodedData.isNullOrEmpty()) { + val envelope = SignalServiceProtos.Envelope.newBuilder() + .setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE) + .setSource(message.sender!!) + .setSourceDevice(1) + .setContent(message.toProto().toByteString()) + .setTimestampMs(message.sentTimestamp) + .build() + envelopes.add(Triple( message.serverID, envelope, message.reactions)) } - .chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER) - .forEach { params -> - JobQueue.shared.add(batchMessageJobFactory.create( - params, - fromCommunity = threadAddress - )) + } + + envelopes.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { list -> + val parameters = list.map { (serverId, message, reactions) -> + MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId, reactions = reactions) } + JobQueue.shared.add(batchMessageJobFactory.create( + parameters, + fromCommunity = threadAddress + )) + } - if (messages.isNotEmpty()) { + if (envelopes.isNotEmpty()) { JobQueue.shared.add(trimThreadJobFactory.create(threadId)) } } diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 873ae274e0..03620cb5f3 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -41,6 +41,7 @@ import org.session.libsession.utilities.UserConfigType import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Snode +import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager import org.thoughtcrime.securesms.util.NetworkConnectivity import kotlin.time.Duration.Companion.days @@ -58,6 +59,7 @@ class Poller @AssistedInject constructor( private val networkConnectivity: NetworkConnectivity, private val batchMessageReceiveJobFactory: BatchMessageReceiveJob.Factory, private val snodeClock: SnodeClock, + private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, @Assisted scope: CoroutineScope ) { private val userPublicKey: String @@ -119,7 +121,7 @@ class Poller @AssistedInject constructor( // To migrate to multi part config, we'll need to fetch all the config messages so we // get the chance to process those multipart messages again... lokiApiDatabase.clearLastMessageHashesByNamespaces(*allConfigNamespaces) - lokiApiDatabase.clearReceivedMessageHashValuesByNamespaces(*allConfigNamespaces) + receivedMessageHashDatabase.removeAllByNamespaces(*allConfigNamespaces) preferences.migratedToMultiPartConfig = true } @@ -220,12 +222,11 @@ class Poller @AssistedInject constructor( namespace = Namespace.DEFAULT() ) - SnodeAPI.removeDuplicates( - publicKey = userPublicKey, + receivedMessageHashDatabase.removeDuplicates( + swarmPublicKey = userPublicKey, messages = messages, messageHashGetter = { it.hash }, namespace = Namespace.DEFAULT(), - updateStoredHashes = true ).asSequence() .map { msg -> MessageReceiveParameters( @@ -252,12 +253,11 @@ class Poller @AssistedInject constructor( newValue = messages.maxBy { it.timestamp }.hash, namespace = namespace ) - SnodeAPI.removeDuplicates( - publicKey = userPublicKey, + receivedMessageHashDatabase.removeDuplicates( + swarmPublicKey = userPublicKey, messages = messages, messageHashGetter = { it.hash }, namespace = namespace, - updateStoredHashes = true ).map { m -> ConfigMessage(data = m.data, hash = m.hash, timestamp = m.timestamp.toEpochMilli()) } diff --git a/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt index 977ea0fb45..76f45419ab 100644 --- a/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -852,43 +852,6 @@ object SnodeAPI { ) } - /** - * - * - * TODO Use a db transaction, synchronizing is sufficient for now because - * database#setReceivedMessageHashValues is only called here. - */ - @Synchronized - fun removeDuplicates( - publicKey: String, - messages: List, - messageHashGetter: (M) -> String?, - namespace: Int, - updateStoredHashes: Boolean - ): List { - val hashValues = database.getReceivedMessageHashValues(publicKey, namespace)?.toMutableSet() ?: mutableSetOf() - return messages - .filter { message -> - val hash = messageHashGetter(message) - if (hash == null) { - Log.d("Loki", "Missing hash value for message: ${message?.prettifiedDescription()}.") - return@filter false - } - - val isNew = hashValues.add(hash) - - if (!isNew) { - Log.d("Loki", "Duplicate message hash: $hash.") - } - - isNew - } - .also { - if (updateStoredHashes && it.isNotEmpty()) { - database.setReceivedMessageHashValues(publicKey, hashValues, namespace) - } - } - } @Suppress("UNCHECKED_CAST") private fun parseDeletions(userPublicKey: String, timestamp: Long, rawResponse: RawResponse): Map = diff --git a/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt b/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt index 9d0d4a6241..c855d52a94 100644 --- a/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt +++ b/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt @@ -20,11 +20,6 @@ interface LokiAPIDatabaseProtocol { fun clearLastMessageHashes(publicKey: String) fun clearLastMessageHashesByNamespaces(vararg namespaces: Int) fun clearAllLastMessageHashes() - fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set? - fun setReceivedMessageHashValues(publicKey: String, newValue: Set, namespace: Int) - fun clearReceivedMessageHashValues(publicKey: String) - fun clearReceivedMessageHashValues() - fun clearReceivedMessageHashValuesByNamespaces(vararg namespaces: Int) fun getAuthToken(server: String): String? fun setAuthToken(server: String, newValue: String?) fun getLastMessageServerID(room: String, server: String): Long? diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java b/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java index 22d9fe9310..567dc491c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java @@ -28,12 +28,14 @@ public class TypingStatusSender { private final Map selfTypingTimers; private final ThreadDatabase threadDatabase; private final RecipientRepository recipientRepository; + private final MessageSender messageSender; @Inject - public TypingStatusSender(ThreadDatabase threadDatabase, RecipientRepository recipientRepository) { + public TypingStatusSender(ThreadDatabase threadDatabase, RecipientRepository recipientRepository, MessageSender messageSender) { this.threadDatabase = threadDatabase; this.recipientRepository = recipientRepository; this.selfTypingTimers = new HashMap<>(); + this.messageSender = messageSender; } public synchronized void onTypingStarted(long threadId) { @@ -94,7 +96,7 @@ private void sendTyping(long threadId, boolean typingStarted) { } else { typingIndicator = new TypingIndicator(TypingIndicator.Kind.STOPPED); } - MessageSender.send(typingIndicator, recipient.getAddress()); + messageSender.send(typingIndicator, recipient.getAddress()); } private class StartRunnable implements Runnable { diff --git a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt index 4fae3a59fb..d2eed7ef9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt @@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase import org.thoughtcrime.securesms.database.RecipientSettingsDatabase import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.ThreadDatabase @@ -81,6 +82,7 @@ class ConfigToDatabaseSync @Inject constructor( private val groupMemberDatabase: GroupMemberDatabase, private val communityDatabase: CommunityDatabase, private val lokiAPIDatabase: LokiAPIDatabase, + private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, private val clock: SnodeClock, private val preferences: TextSecurePreferences, private val conversationRepository: ConversationRepository, @@ -195,7 +197,7 @@ class ConfigToDatabaseSync @Inject constructor( private fun deleteGroupData(address: Address.Group) { lokiAPIDatabase.clearLastMessageHashes(address.accountId.hexString) - lokiAPIDatabase.clearReceivedMessageHashValues(address.accountId.hexString) + receivedMessageHashDatabase.removeAllByPublicKey(address.accountId.hexString) } private fun onLegacyGroupAdded( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt index 6a16e68a7d..eadffadb3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt @@ -49,6 +49,8 @@ class LokiAPIDatabase(context: Context, helper: Provider) : = "CREATE TABLE $legacyLastMessageHashValueTable2 ($snode TEXT, $publicKey TEXT, $lastMessageHashValue TEXT, PRIMARY KEY ($snode, $publicKey));" // Received message hash values private const val legacyReceivedMessageHashValuesTable3 = "received_message_hash_values_table_3" + + @Deprecated("This table is deleted and replaced by ReceivedMessageHashDatabase") private const val receivedMessageHashValuesTable = "session_received_message_hash_values_table" private const val receivedMessageHashValues = "received_message_hash_values" private const val receivedMessageHashNamespace = "received_message_namespace" @@ -128,6 +130,7 @@ class LokiAPIDatabase(context: Context, helper: Provider) : const val INSERT_LAST_HASH_DATA = "INSERT OR IGNORE INTO $lastMessageHashValueTable2($snode, $publicKey, $lastMessageHashValue) SELECT $snode, $publicKey, $lastMessageHashValue FROM $legacyLastMessageHashValueTable2;" const val DROP_LEGACY_LAST_HASH = "DROP TABLE $legacyLastMessageHashValueTable2;" + @Deprecated("This table is deleted and replaced by ReceivedMessageHashDatabase, keeping here just for migration purpose") const val UPDATE_RECEIVED_INCLUDE_NAMESPACE_COMMAND = """ CREATE TABLE IF NOT EXISTS $receivedMessageHashValuesTable( $publicKey STRING, $receivedMessageHashValues TEXT, $receivedMessageHashNamespace INTEGER DEFAULT 0, PRIMARY KEY ($publicKey, $receivedMessageHashNamespace) @@ -311,43 +314,6 @@ class LokiAPIDatabase(context: Context, helper: Provider) : database.delete(lastMessageHashValueTable2, null, null) } - override fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set? { - val database = readableDatabase - val query = "${Companion.publicKey} = ? AND ${Companion.receivedMessageHashNamespace} = ?" - return database.get(receivedMessageHashValuesTable, query, arrayOf( publicKey, namespace.toString() )) { cursor -> - val receivedMessageHashValuesAsString = cursor.getString(cursor.getColumnIndexOrThrow(Companion.receivedMessageHashValues)) - receivedMessageHashValuesAsString.split("-").toSet() - } - } - - override fun setReceivedMessageHashValues(publicKey: String, newValue: Set, namespace: Int) { - val database = writableDatabase - val receivedMessageHashValuesAsString = newValue.joinToString("-") - val row = wrap(mapOf( - Companion.publicKey to publicKey, - Companion.receivedMessageHashValues to receivedMessageHashValuesAsString, - Companion.receivedMessageHashNamespace to namespace.toString() - )) - val query = "${Companion.publicKey} = ? AND $receivedMessageHashNamespace = ?" - database.insertOrUpdate(receivedMessageHashValuesTable, row, query, arrayOf( publicKey, namespace.toString() )) - } - - override fun clearReceivedMessageHashValues(publicKey: String) { - writableDatabase - .delete(receivedMessageHashValuesTable, "${Companion.publicKey} = ?", arrayOf(publicKey)) - } - - override fun clearReceivedMessageHashValues() { - val database = writableDatabase - database.delete(receivedMessageHashValuesTable, null, null) - } - - override fun clearReceivedMessageHashValuesByNamespaces(vararg namespaces: Int) { - // Note that we don't use SQL parameter as the given namespaces are integer anyway so there's little chance of SQL injection - writableDatabase - .delete(receivedMessageHashValuesTable, "$receivedMessageHashNamespace IN (${namespaces.joinToString(",")})", null) - } - override fun getAuthToken(server: String): String? { val database = readableDatabase return database.get(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt new file mode 100644 index 0000000000..a2563b073e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt @@ -0,0 +1,126 @@ +package org.thoughtcrime.securesms.database + +import android.content.Context +import androidx.sqlite.db.SupportSQLiteDatabase +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.serialization.json.Json +import org.session.libsession.snode.SnodeAPI.database +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.prettifiedDescription +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.util.asSequence +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton + +@Singleton +class ReceivedMessageHashDatabase @Inject constructor( + @ApplicationContext context: Context, + databaseHelper: Provider, + private val json: Json, +) : Database(context, databaseHelper) { + fun removeAllByNamespaces(vararg namespaces: Int) { + //language=roomsql + writableDatabase.rawExecSQL(""" + DELETE FROM received_messages + WHERE namespace IN (SELECT value FROM json_each(?)) + """, json.encodeToString(namespaces)) + } + + fun removeAllByPublicKey(publicKey: String) { + //language=roomsql + writableDatabase.rawExecSQL(""" + DELETE FROM received_messages + WHERE swarm_pub_key = ? + """, publicKey) + } + + fun removeAll() { + //language=roomsql + writableDatabase.rawExecSQL("DELETE FROM received_messages WHERE 1") + } + + fun removeDuplicatedHashes( + swarmPublicKey: String, + namespace: Int, + hashes: Collection + ): Set { + val hashesAsJsonText = json.encodeToString(hashes) + + //language=roomsql + return writableDatabase.rawQuery(""" + INSERT OR IGNORE INTO received_messages (swarm_pub_key, namespace, hash) + SELECT ?, ?, value + FROM json_each(?) + RETURNING hash + """, swarmPublicKey, namespace, hashesAsJsonText).use { cursor -> + cursor.asSequence() + .mapTo(hashSetOf()) { it.getString(0) } + } + } + + fun removeDuplicates( + swarmPublicKey: String, + messages: List, + messageHashGetter: (M) -> String?, + namespace: Int, + ): List { + val newMessageHashes = removeDuplicatedHashes( + swarmPublicKey = swarmPublicKey, + namespace = namespace, + hashes = messages.mapNotNull(messageHashGetter) + ) + + return messages.filter { message -> + val hash = messageHashGetter(message) + hash != null && hash in newMessageHashes + } + } + + + companion object { + fun createAndMigrateTable(db: SupportSQLiteDatabase) { + //language=roomsql + db.execSQL(""" + CREATE TABLE IF NOT EXISTS received_messages( + swarm_pub_key TEXT NOT NULL, + namespace INTEGER NOT NULL, + hash TEXT NOT NULL, + PRIMARY KEY (swarm_pub_key, namespace, hash) + ) WITHOUT ROWID; + """) + + //language=roomsql + db.compileStatement(""" + INSERT OR IGNORE INTO received_messages (swarm_pub_key, namespace, hash) + VALUES (?, ?, ?) + """).use { stmt -> + + //language=roomsql + db.query(""" + SELECT public_key, received_message_hash_values, received_message_namespace + FROM session_received_message_hash_values_table + """, arrayOf()).use { cursor -> + while (cursor.moveToNext()) { + val publicKey = cursor.getString(0) + val hashValuesString = cursor.getString(1) + val namespace = cursor.getInt(2) + + val hashValues = hashValuesString.splitToSequence('-') + + for (hash in hashValues) { + stmt.bindString(1, publicKey) + stmt.bindLong(2, namespace.toLong()) + stmt.bindString(3, hash) + stmt.execute() + stmt.clearBindings() + } + } + } + } + + //language=roomsql + db.execSQL("DROP TABLE session_received_message_hash_values_table") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 4bed3ca3c0..8c85d55df5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -772,7 +772,7 @@ public boolean isRead(long threadId) { public boolean markAllAsRead(long threadId, long lastSeenTime, boolean force, boolean updateNotifications) { if (mmsSmsDatabase.get().getConversationCount(threadId) <= 0 && !force) return false; List messages = setRead(threadId, lastSeenTime); - markReadProcessor.get().process(context, messages); + markReadProcessor.get().process(messages); if(updateNotifications) messageNotifier.get().updateNotification(context, threadId); return setLastSeen(threadId, lastSeenTime); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 0ab54cbdca..3816b3920f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.PushRegistrationDatabase; import org.thoughtcrime.securesms.database.ReactionDatabase; +import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientSettingsDatabase; import org.thoughtcrime.securesms.database.SearchDatabase; @@ -102,9 +103,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV53 = 74; private static final int lokiV54 = 75; private static final int lokiV55 = 76; + private static final int lokiV56 = 77; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV55; + private static final int DATABASE_VERSION = lokiV56; private static final int MIN_DATABASE_VERSION = lokiV7; public static final String DATABASE_NAME = "session.db"; @@ -266,6 +268,8 @@ public void onCreate(SQLiteDatabase db) { db.execSQL(MmsDatabase.ADD_LAST_MESSAGE_INDEX); executeStatements(db, PushRegistrationDatabase.Companion.createTableStatements()); + + ReceivedMessageHashDatabase.Companion.createAndMigrateTable(db); } @Override @@ -604,6 +608,10 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { executeStatements(db, PushRegistrationDatabase.Companion.createTableStatements()); } + if (oldVersion < lokiV56) { + ReceivedMessageHashDatabase.Companion.createAndMigrateTable(db); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index 753e94098d..89efd74846 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.configs.ConfigUploader import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.dependencies.ConfigFactory @@ -84,6 +85,7 @@ class GroupManagerV2Impl @Inject constructor( private val clock: SnodeClock, private val messageDataProvider: MessageDataProvider, private val lokiAPIDatabase: LokiAPIDatabase, + private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, private val configUploader: ConfigUploader, private val scope: GroupScope, private val groupPollerManager: GroupPollerManager, @@ -885,7 +887,7 @@ class GroupManagerV2Impl @Inject constructor( // Clear all polling states lokiAPIDatabase.clearLastMessageHashes(groupId.hexString) - lokiAPIDatabase.clearReceivedMessageHashValues(groupId.hexString) + receivedMessageHashDatabase.removeAllByPublicKey(groupId.hexString) SessionMetaProtocol.clearReceivedMessages() configFactory.deleteGroupConfigs(groupId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index a1360fc30d..6988210842 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -36,6 +36,7 @@ import org.session.libsignal.exceptions.NonRetryableException import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Snode +import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager import org.thoughtcrime.securesms.util.getRootCause import java.time.Instant @@ -52,6 +53,7 @@ class GroupPoller @AssistedInject constructor( private val appVisibilityManager: AppVisibilityManager, private val groupRevokedMessageHandler: GroupRevokedMessageHandler, private val batchMessageReceiveJobFactory: BatchMessageReceiveJob.Factory, + private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, ) { companion object { private const val POLL_INTERVAL = 3_000L @@ -444,12 +446,11 @@ class GroupPoller @AssistedInject constructor( namespace = Namespace.GROUP_MESSAGES() ) - SnodeAPI.removeDuplicates( - publicKey = groupId.hexString, + receivedMessageHashDatabase.removeDuplicates( + swarmPublicKey = groupId.hexString, namespace = Namespace.GROUP_MESSAGES(), messages = messages, messageHashGetter = { it.hash }, - updateStoredHashes = true ).asSequence() .map { msg -> MessageReceiveParameters( diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java index 4a72a4320a..42b3d406dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java @@ -80,6 +80,9 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver { @Inject MarkReadProcessor markReadProcessor; + @Inject + MessageSender messageSender; + @SuppressLint("StaticFieldLeak") @Override public void onReceive(final Context context, Intent intent) @@ -110,7 +113,7 @@ protected Void doInBackground(Void... params) { VisibleMessage message = new VisibleMessage(); message.setText(responseText.toString()); message.setSentTimestamp(SnodeAPI.getNowWithOffset()); - MessageSender.send(message, address); + messageSender.send(message, address); ExpiryMode expiryMode = recipientRepository.getRecipientSync(address).getExpiryMode(); long expiresInMillis = expiryMode.getExpiryMillis(); long expireStartedAt = expiryMode instanceof ExpiryMode.AfterSend ? message.getSentTimestamp() : 0L; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index cbcd32e353..9d10f23a9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -78,6 +78,8 @@ public class RemoteReplyReceiver extends BroadcastReceiver { RecipientRepository recipientRepository; @Inject MarkReadProcessor markReadProcessor; + @Inject + MessageSender messageSender; @SuppressLint("StaticFieldLeak") @Override @@ -112,7 +114,7 @@ protected Void doInBackground(Void... params) { OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, address, Collections.emptyList(), null, null, expiresInMillis, 0); try { message.setId(new MessageId(mmsDatabase.insertMessageOutbox(reply, threadId, false, true), true)); - MessageSender.send(message, address); + messageSender.send(message, address); } catch (MmsException e) { Log.w(TAG, e); } @@ -121,7 +123,7 @@ protected Void doInBackground(Void... params) { case SecureMessage: { OutgoingTextMessage reply = OutgoingTextMessage.from(message, address, expiresInMillis, expireStartedAt); message.setId(new MessageId(smsDatabase.insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), true), false)); - MessageSender.send(message, address); + messageSender.send(message, address); break; } default: diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt index 5602ccb9d7..6c9f34166a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt @@ -9,6 +9,7 @@ import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase import org.thoughtcrime.securesms.util.VersionDataFetcher import javax.inject.Inject import javax.inject.Singleton @@ -18,7 +19,8 @@ class CreateAccountManager @Inject constructor( private val application: Application, private val prefs: TextSecurePreferences, private val versionDataFetcher: VersionDataFetcher, - private val configFactory: ConfigFactoryProtocol + private val configFactory: ConfigFactoryProtocol, + private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, ) { private val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage @@ -27,7 +29,7 @@ class CreateAccountManager @Inject constructor( // This is here to resolve a case where the app restarts before a user completes onboarding // which can result in an invalid database state database.clearAllLastMessageHashes() - database.clearReceivedMessageHashValues() + receivedMessageHashDatabase.removeAll() val keyPairGenerationResult = KeyPairUtilities.generate() val seed = keyPairGenerationResult.seed diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt index 489742a9a2..c7dd30f462 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt @@ -11,6 +11,7 @@ import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.util.VersionDataFetcher import javax.inject.Inject @@ -20,7 +21,8 @@ import javax.inject.Singleton class LoadAccountManager @Inject constructor( @dagger.hilt.android.qualifiers.ApplicationContext private val context: Context, private val prefs: TextSecurePreferences, - private val versionDataFetcher: VersionDataFetcher + private val versionDataFetcher: VersionDataFetcher, + private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, ) { private val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage @@ -37,7 +39,7 @@ class LoadAccountManager @Inject constructor( // This is here to resolve a case where the app restarts before a user completes onboarding // which can result in an invalid database state database.clearAllLastMessageHashes() - database.clearReceivedMessageHashValues() + receivedMessageHashDatabase.removeAll() // RestoreActivity handles seed this way val keyPairGenerationResult = KeyPairUtilities.generate(seed) From 22d54f451b5d15fa06cc854178d166de0e6b115b Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:25:14 +1100 Subject: [PATCH 09/19] Message timestamp is optional --- .../sending_receiving/pollers/Poller.kt | 8 +++++--- .../org/session/libsession/snode/SnodeAPI.kt | 17 +++++++++++++---- .../libsession/snode/model/MessageResponses.kt | 2 +- .../database/ReceivedMessageHashDatabase.kt | 3 --- .../securesms/groups/GroupPoller.kt | 4 ++-- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 03620cb5f3..a9e6e5ee63 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -44,6 +44,7 @@ import org.session.libsignal.utilities.Snode import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager import org.thoughtcrime.securesms.util.NetworkConnectivity +import java.time.Instant import kotlin.time.Duration.Companion.days private const val TAG = "Poller" @@ -218,7 +219,8 @@ class Poller @AssistedInject constructor( lokiApiDatabase.setLastMessageHashValue( snode = snode, publicKey = userPublicKey, - newValue = messages.maxBy { it.timestamp }.hash, + newValue = messages + .maxBy { it.timestamp ?: Instant.EPOCH }.hash, namespace = Namespace.DEFAULT() ) @@ -250,7 +252,7 @@ class Poller @AssistedInject constructor( lokiApiDatabase.setLastMessageHashValue( snode = snode, publicKey = userPublicKey, - newValue = messages.maxBy { it.timestamp }.hash, + newValue = messages.maxBy { it.timestamp ?: Instant.EPOCH }.hash, namespace = namespace ) receivedMessageHashDatabase.removeDuplicates( @@ -259,7 +261,7 @@ class Poller @AssistedInject constructor( messageHashGetter = { it.hash }, namespace = namespace, ).map { m -> - ConfigMessage(data = m.data, hash = m.hash, timestamp = m.timestamp.toEpochMilli()) + ConfigMessage(data = m.data, hash = m.hash, timestamp = m.timestamp!!.toEpochMilli()) } } else emptyList() diff --git a/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt index 76f45419ab..b227885f7e 100644 --- a/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/app/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.selects.select import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromStream import network.loki.messenger.libsession_util.ED25519 import network.loki.messenger.libsession_util.Hash import network.loki.messenger.libsession_util.SessionEncrypt @@ -159,13 +160,18 @@ object SnodeAPI { method: Snode.Method, snode: Snode, parameters: Map, - responseClass: Class, + responseDeserializationStrategy: DeserializationStrategy, publicKey: String? = null, version: Version = Version.V3 ): Res = when { useOnionRequests -> { val resp = OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey).await() - JsonUtil.fromJson(resp.body ?: throw Error.Generic, responseClass) + (resp.body ?: throw Error.Generic).inputStream().use { inputStream -> + MessagingModuleConfiguration.shared.json.decodeFromStream( + deserializer = responseDeserializationStrategy, + stream = inputStream + ) + } } else -> HTTP.execute( @@ -176,7 +182,10 @@ object SnodeAPI { this["params"] = parameters } ).toString().let { - JsonUtil.fromJson(it, responseClass) + MessagingModuleConfiguration.shared.json.decodeFromString( + deserializer = responseDeserializationStrategy, + string = it + ) } } @@ -626,7 +635,7 @@ object SnodeAPI { method = if (sequence) Snode.Method.Sequence else Snode.Method.Batch, snode = snode, parameters = mapOf("requests" to requests), - responseClass = BatchResponse::class.java, + responseDeserializationStrategy = BatchResponse.serializer(), publicKey = publicKey ).also { resp -> // If there's a unsuccessful response, go through specific logic to handle diff --git a/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt b/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt index e4d432376f..4e8d073b47 100644 --- a/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt +++ b/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt @@ -22,7 +22,7 @@ data class RetrieveMessageResponse( val hash: String, @Serializable(InstantAsMillisSerializer::class) @SerialName("t") - val timestamp: Instant, + val timestamp: Instant? = null, @SerialName("data") val dataB64: String? = null, ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt index a2563b073e..a97d4dfb05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt @@ -4,9 +4,6 @@ import android.content.Context import androidx.sqlite.db.SupportSQLiteDatabase import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.serialization.json.Json -import org.session.libsession.snode.SnodeAPI.database -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.prettifiedDescription import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.util.asSequence import javax.inject.Inject diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index 6988210842..d3efdd2c6b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -388,7 +388,7 @@ class GroupPoller @AssistedInject constructor( } private fun RetrieveMessageResponse.Message.toConfigMessage(): ConfigMessage { - return ConfigMessage(hash, data, timestamp.toEpochMilli()) + return ConfigMessage(hash, data, timestamp!!.toEpochMilli()) } private fun saveLastMessageHash( @@ -442,7 +442,7 @@ class GroupPoller @AssistedInject constructor( lokiApiDatabase.setLastMessageHashValue( snode = snode, publicKey = groupId.hexString, - newValue = messages.maxBy { it.timestamp }.hash, + newValue = messages.maxBy { it.timestamp ?: Instant.EPOCH }.hash, namespace = Namespace.GROUP_MESSAGES() ) From c1de259f24f85b093dac4c10a81085c552846566 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:35:39 +1100 Subject: [PATCH 10/19] Timestamp property on message --- .../messaging/sending_receiving/pollers/Poller.kt | 6 +++--- .../libsession/snode/model/MessageResponses.kt | 14 +++++++++++++- .../thoughtcrime/securesms/groups/GroupPoller.kt | 6 +++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index a9e6e5ee63..aa9c6381fa 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -220,7 +220,7 @@ class Poller @AssistedInject constructor( snode = snode, publicKey = userPublicKey, newValue = messages - .maxBy { it.timestamp ?: Instant.EPOCH }.hash, + .maxBy { it.timestamp }.hash, namespace = Namespace.DEFAULT() ) @@ -252,7 +252,7 @@ class Poller @AssistedInject constructor( lokiApiDatabase.setLastMessageHashValue( snode = snode, publicKey = userPublicKey, - newValue = messages.maxBy { it.timestamp ?: Instant.EPOCH }.hash, + newValue = messages.maxBy { it.timestamp }.hash, namespace = namespace ) receivedMessageHashDatabase.removeDuplicates( @@ -261,7 +261,7 @@ class Poller @AssistedInject constructor( messageHashGetter = { it.hash }, namespace = namespace, ).map { m -> - ConfigMessage(data = m.data, hash = m.hash, timestamp = m.timestamp!!.toEpochMilli()) + ConfigMessage(data = m.data, hash = m.hash, timestamp = m.timestamp.toEpochMilli()) } } else emptyList() diff --git a/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt b/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt index 4e8d073b47..2c7b6f6d7e 100644 --- a/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt +++ b/app/src/main/java/org/session/libsession/snode/model/MessageResponses.kt @@ -20,14 +20,26 @@ data class RetrieveMessageResponse( @Serializable data class Message( val hash: String, + + // Some messages use "t" as timestamp field @Serializable(InstantAsMillisSerializer::class) @SerialName("t") - val timestamp: Instant? = null, + private val t1: Instant? = null, + + // Some messages use "timestamp" as timestamp field + @Serializable(InstantAsMillisSerializer::class) + @SerialName("timestamp") + private val t2: Instant? = null, + @SerialName("data") val dataB64: String? = null, ) { val data: ByteArray by lazy(LazyThreadSafetyMode.NONE) { Base64.decode(dataB64, Base64.DEFAULT) } + + val timestamp: Instant get() = requireNotNull(t1 ?: t2) { + "Message timestamp is missing" + } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index d3efdd2c6b..3284b92755 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -307,7 +307,7 @@ class GroupPoller @AssistedInject constructor( maxSize = null, ), responseType = RetrieveMessageResponse.serializer() - ).messages.filterNotNull() + ).messages } } @@ -388,7 +388,7 @@ class GroupPoller @AssistedInject constructor( } private fun RetrieveMessageResponse.Message.toConfigMessage(): ConfigMessage { - return ConfigMessage(hash, data, timestamp!!.toEpochMilli()) + return ConfigMessage(hash, data, timestamp.toEpochMilli()) } private fun saveLastMessageHash( @@ -442,7 +442,7 @@ class GroupPoller @AssistedInject constructor( lokiApiDatabase.setLastMessageHashValue( snode = snode, publicKey = groupId.hexString, - newValue = messages.maxBy { it.timestamp ?: Instant.EPOCH }.hash, + newValue = messages.maxBy { it.timestamp }.hash, namespace = Namespace.GROUP_MESSAGES() ) From f2c3d7338376e6e4e0143c03a09cfb72ebba50a2 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:24:14 +1100 Subject: [PATCH 11/19] Message receiving WIP --- .../sending_receiving/pollers/Poller.kt | 106 +++++++++--------- .../database/ReceivedMessageHashDatabase.kt | 62 ++++++---- .../securesms/groups/GroupPoller.kt | 48 +++----- 3 files changed, 111 insertions(+), 105 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index aa9c6381fa..616c0ac52a 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -211,74 +211,52 @@ class Poller @AssistedInject constructor( } } - private fun processPersonalMessages(snode: Snode, messages: List) { + private fun processPersonalMessages(messages: List) { if (messages.isEmpty()) { + Log.d(TAG, "No personal messages to process") return } - lokiApiDatabase.setLastMessageHashValue( - snode = snode, - publicKey = userPublicKey, - newValue = messages - .maxBy { it.timestamp }.hash, - namespace = Namespace.DEFAULT() - ) + Log.d(TAG, "Received ${messages.size} personal messages from snode") - receivedMessageHashDatabase.removeDuplicates( + receivedMessageHashDatabase.withRemovedDuplicateMessages( swarmPublicKey = userPublicKey, + namespace = Namespace.DEFAULT(), messages = messages, messageHashGetter = { it.hash }, - namespace = Namespace.DEFAULT(), - ).asSequence() - .map { msg -> - MessageReceiveParameters( - data = msg.data, - serverHash = msg.hash, - ) - } - .chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER) - .forEach { chunk -> - JobQueue.shared.add(batchMessageReceiveJobFactory.create( - messages = chunk, - fromCommunity = null - )) - } + ) { filtered -> + Log.d(TAG, "About to process ${filtered.size} new personal messages") + } } - private fun processConfig(snode: Snode, messages: List, forConfig: UserConfigType) { - Log.d(TAG, "Received ${messages.size} messages for $forConfig") - val namespace = forConfig.namespace - val processed = if (messages.isNotEmpty()) { - lokiApiDatabase.setLastMessageHashValue( - snode = snode, - publicKey = userPublicKey, - newValue = messages.maxBy { it.timestamp }.hash, - namespace = namespace - ) - receivedMessageHashDatabase.removeDuplicates( + private fun processConfig(messages: List, forConfig: UserConfigType) { + if (messages.isEmpty()) { + Log.d(TAG, "No messages to process for $forConfig") + return + } + + try { + receivedMessageHashDatabase.withRemovedDuplicateMessages( swarmPublicKey = userPublicKey, + namespace = forConfig.namespace, messages = messages, messageHashGetter = { it.hash }, - namespace = namespace, - ).map { m -> - ConfigMessage(data = m.data, hash = m.hash, timestamp = m.timestamp.toEpochMilli()) - } - } else emptyList() - - Log.d(TAG, "About to process ${processed.size} messages for $forConfig") - - if (processed.isEmpty()) return + ) { filtered -> + if (filtered.isNotEmpty()) { + configFactory.mergeUserConfigs( + userConfigType = forConfig, + messages = filtered + .mapTo(arrayListOf()) { m -> + ConfigMessage(data = m.data, hash = m.hash, timestamp = m.timestamp.toEpochMilli()) + } + ) + } - try { - configFactory.mergeUserConfigs( - userConfigType = forConfig, - messages = processed, - ) + Log.d(TAG, "Completed processing ${filtered.size} messages for $forConfig") + } } catch (e: Exception) { Log.e(TAG, "Error while merging user configs", e) } - - Log.d(TAG, "Completed processing messages for $forConfig") } @@ -377,7 +355,18 @@ class Poller @AssistedInject constructor( continue } - processConfig(snode, result.getOrThrow().messages, configType) + val messages = result.getOrThrow().messages + processConfig(messages = messages, forConfig = configType) + + if (messages.isNotEmpty()) { + lokiApiDatabase.setLastMessageHashValue( + snode = snode, + publicKey = userPublicKey, + newValue = messages + .maxBy { it.timestamp }.hash, + namespace = configType.namespace + ) + } } // Process the messages if we requested them @@ -386,7 +375,18 @@ class Poller @AssistedInject constructor( if (result.isFailure) { Log.e(TAG, "Error while fetching messages", result.exceptionOrNull()) } else { - processPersonalMessages(snode, result.getOrThrow().messages) + val messages = result.getOrThrow().messages + processPersonalMessages(messages) + + if (messages.isNotEmpty()) { + lokiApiDatabase.setLastMessageHashValue( + snode = snode, + publicKey = userPublicKey, + newValue = messages + .maxBy { it.timestamp }.hash, + namespace = Namespace.DEFAULT() + ) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt index a97d4dfb05..5dcc41a20a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database import android.content.Context import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.transaction import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.serialization.json.Json import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper @@ -37,7 +38,7 @@ class ReceivedMessageHashDatabase @Inject constructor( writableDatabase.rawExecSQL("DELETE FROM received_messages WHERE 1") } - fun removeDuplicatedHashes( + private fun removeDuplicatedHashes( swarmPublicKey: String, namespace: Int, hashes: Collection @@ -46,34 +47,57 @@ class ReceivedMessageHashDatabase @Inject constructor( //language=roomsql return writableDatabase.rawQuery(""" - INSERT OR IGNORE INTO received_messages (swarm_pub_key, namespace, hash) - SELECT ?, ?, value - FROM json_each(?) - RETURNING hash - """, swarmPublicKey, namespace, hashesAsJsonText).use { cursor -> + SELECT ea.value FROM json_each(?) ea + WHERE NOT EXISTS ( + SELECT 1 FROM received_messages m + WHERE m.swarm_pub_key = ? AND m.namespace = ? AND m.hash = ea.value + ) + """, hashesAsJsonText, swarmPublicKey, namespace).use { cursor -> cursor.asSequence() .mapTo(hashSetOf()) { it.getString(0) } } } - fun removeDuplicates( + /** + * Filters out messages with duplicate hashes from the provided list, performs the given action + * on the new messages, and then adds the new message hashes to the database. + */ + fun withRemovedDuplicateMessages( swarmPublicKey: String, - messages: List, - messageHashGetter: (M) -> String?, namespace: Int, - ): List { - val newMessageHashes = removeDuplicatedHashes( - swarmPublicKey = swarmPublicKey, - namespace = namespace, - hashes = messages.mapNotNull(messageHashGetter) - ) - - return messages.filter { message -> - val hash = messageHashGetter(message) - hash != null && hash in newMessageHashes + messages: List, + messageHashGetter: (M) -> String, + performOnNewMessages: (List) -> T, + ): T { + return writableDatabase.transaction { + val newMessageHashes = removeDuplicatedHashes( + swarmPublicKey = swarmPublicKey, + namespace = namespace, + hashes = messages.map(messageHashGetter) + ) + + val ret = performOnNewMessages( + messages.filter { m -> messageHashGetter(m) in newMessageHashes } + ) + + addHashes( + swarmPublicKey = swarmPublicKey, + namespace = namespace, + hashes = newMessageHashes + ) + + ret } } + private fun addHashes(swarmPublicKey: String, namespace: Int, hashes: Collection) { + //language=roomsql + writableDatabase.rawExecSQL(""" + INSERT OR IGNORE INTO received_messages (swarm_pub_key, namespace, hash) + SELECT ?, ?, value FROM json_each(?) + """, swarmPublicKey, namespace, json.encodeToString(hashes)) + } + companion object { fun createAndMigrateTable(db: SupportSQLiteDatabase) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index 3284b92755..0c35213937 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -21,9 +21,6 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import network.loki.messenger.libsession_util.Namespace import org.session.libsession.messaging.jobs.BatchMessageReceiveJob -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.MessageReceiveParameters -import org.session.libsession.messaging.messages.Destination import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeClock import org.session.libsession.snode.model.BatchResponse @@ -327,7 +324,16 @@ class GroupPoller @AssistedInject constructor( } val regularMessages = groupMessageRetrieval.await() - handleMessages(regularMessages.messages, snode) + handleMessages(regularMessages.messages) + + if (regularMessages.messages.isNotEmpty()) { + lokiApiDatabase.setLastMessageHashValue( + snode = snode, + publicKey = groupId.hexString, + newValue = regularMessages.messages.maxBy { it.timestamp }.hash, + namespace = Namespace.GROUP_MESSAGES() + ) + } } // Revoke message must be handled regardless, and at the end @@ -434,43 +440,19 @@ class GroupPoller @AssistedInject constructor( ) } - private fun handleMessages(messages: List, snode: Snode) { + private fun handleMessages(messages: List) { if (messages.isEmpty()) { return } - lokiApiDatabase.setLastMessageHashValue( - snode = snode, - publicKey = groupId.hexString, - newValue = messages.maxBy { it.timestamp }.hash, - namespace = Namespace.GROUP_MESSAGES() - ) - - receivedMessageHashDatabase.removeDuplicates( + Log.d(TAG, "${groupId}: Received ${messages.size} group messages from snode") + receivedMessageHashDatabase.withRemovedDuplicateMessages( swarmPublicKey = groupId.hexString, namespace = Namespace.GROUP_MESSAGES(), messages = messages, messageHashGetter = { it.hash }, - ).asSequence() - .map { msg -> - MessageReceiveParameters( - data = msg.data, - serverHash = msg.hash, - closedGroup = Destination.ClosedGroup(groupId.hexString), - ) - } - .chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER) - .forEach { chunk -> - JobQueue.shared.add( - batchMessageReceiveJobFactory.create( - messages = chunk, - fromCommunity = null - ) - ) - } - - if (messages.isNotEmpty()) { - Log.d(TAG, "Received and handled ${messages.size} group messages") + ) { newMessages -> + Log.d(TAG, "${groupId}: Handling ${newMessages.size} new group messages") } } From 2ce99c2f2ce8a6543b73076b68902a5229f77649 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:54:57 +1100 Subject: [PATCH 12/19] Create new path for message processing --- .../messaging/jobs/BatchMessageReceiveJob.kt | 2 +- .../sending_receiving/GroupMessageHandler.kt | 219 +++++++++++ .../sending_receiving/MessageHandler.kt | 14 +- .../MessageRequestResponseHandler.kt | 10 +- .../ReceivedMessageHandler.kt | 1 + .../ReceivedMessageProcessor.kt | 342 ++++++++++++++++++ .../VisibleMessageHandler.kt | 259 +++++++++++++ .../pollers/OpenGroupPoller.kt | 20 +- .../sending_receiving/pollers/Poller.kt | 114 ++++-- .../database/ReceivedMessageHashDatabase.kt | 97 +++-- .../securesms/groups/GroupPoller.kt | 63 +++- .../securesms/notifications/PushReceiver.kt | 156 ++++---- gradle/libs.versions.toml | 2 +- 13 files changed, 1104 insertions(+), 195 deletions(-) create mode 100644 app/src/main/java/org/session/libsession/messaging/sending_receiving/GroupMessageHandler.kt create mode 100644 app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt create mode 100644 app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index 889f0709bf..cdf760a7b7 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -362,7 +362,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( @AssistedFactory abstract class Factory : Job.DeserializeFactory { @Deprecated("New code should try to handle message directly instead of creating this job") - abstract fun create( + protected abstract fun create( messages: List, fromCommunity: Address.Community?, ): BatchMessageReceiveJob diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/GroupMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/GroupMessageHandler.kt new file mode 100644 index 0000000000..abeb569713 --- /dev/null +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/GroupMessageHandler.kt @@ -0,0 +1,219 @@ +package org.session.libsession.messaging.sending_receiving + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import network.loki.messenger.libsession_util.ED25519 +import org.session.libsession.database.StorageProtocol +import org.session.libsession.messaging.groups.GroupManagerV2 +import org.session.libsession.messaging.messages.ProfileUpdateHandler +import org.session.libsession.messaging.messages.control.GroupUpdated +import org.session.libsession.messaging.utilities.MessageAuthentication.buildDeleteMemberContentSignature +import org.session.libsession.messaging.utilities.MessageAuthentication.buildGroupInviteSignature +import org.session.libsession.messaging.utilities.MessageAuthentication.buildInfoChangeSignature +import org.session.libsession.messaging.utilities.MessageAuthentication.buildMemberChangeSignature +import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.IdPrefix +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.dependencies.ManagerScope +import java.security.SignatureException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GroupMessageHandler @Inject constructor( + private val profileUpdateHandler: ProfileUpdateHandler, + private val storage: StorageProtocol, + private val groupManagerV2: GroupManagerV2, + @param:ManagerScope private val scope: CoroutineScope, +) { + fun handleGroupUpdated(message: GroupUpdated, groupId: AccountId?) { + val inner = message.inner + if (groupId == null && + !inner.hasInviteMessage() && !inner.hasPromoteMessage()) { + throw NullPointerException("Message wasn't polled from a closed group!") + } + + // Update profile if needed + ProfileUpdateHandler.Updates.create( + name = message.profile?.displayName, + picUrl = message.profile?.profilePictureURL, + picKey = message.profile?.profileKey, + blocksCommunityMessageRequests = null, + proStatus = null, + profileUpdateTime = null + )?.let { updates -> + profileUpdateHandler.handleProfileUpdate( + senderId = AccountId(message.sender!!), + updates = updates, + fromCommunity = null // Groupv2 is not a community + ) + } + + when { + inner.hasInviteMessage() -> handleNewLibSessionClosedGroupMessage(message) + inner.hasInviteResponse() -> handleInviteResponse(message, groupId!!) + inner.hasPromoteMessage() -> handlePromotionMessage(message) + inner.hasInfoChangeMessage() -> handleGroupInfoChange(message, groupId!!) + inner.hasMemberChangeMessage() -> handleMemberChange(message, groupId!!) + inner.hasMemberLeftMessage() -> handleMemberLeft(message, groupId!!) + inner.hasMemberLeftNotificationMessage() -> handleMemberLeftNotification(message, groupId!!) + inner.hasDeleteMemberContent() -> handleDeleteMemberContent(message, groupId!!) + } + } + + private fun handleNewLibSessionClosedGroupMessage(message: GroupUpdated) { + val storage = storage + val ourUserId = storage.getUserPublicKey()!! + val invite = message.inner.inviteMessage + val groupId = AccountId(invite.groupSessionId) + verifyAdminSignature( + groupSessionId = groupId, + signatureData = invite.adminSignature.toByteArray(), + messageToValidate = buildGroupInviteSignature(AccountId(ourUserId), message.sentTimestamp!!) + ) + + val sender = message.sender!! + val adminId = AccountId(sender) + scope.launch { + try { + groupManagerV2 + .handleInvitation( + groupId = groupId, + groupName = invite.name, + authData = invite.memberAuthData.toByteArray(), + inviter = adminId, + inviterName = message.profile?.displayName, + inviteMessageHash = message.serverHash!!, + inviteMessageTimestamp = message.sentTimestamp!!, + ) + } catch (e: Exception) { + Log.e("GroupUpdated", "Failed to handle invite message", e) + } + } + } + + /** + * Does nothing on successful signature verification, throws otherwise. + * Assumes the signer is using the ed25519 group key signing key + * @param groupSessionId the AccountId of the group to check the signature against + * @param signatureData the byte array supplied to us through a protobuf message from the admin + * @param messageToValidate the expected values used for this signature generation, often something like `INVITE||{inviteeSessionId}||{timestamp}` + * @throws SignatureException if signature cannot be verified with given parameters + */ + private fun verifyAdminSignature(groupSessionId: AccountId, signatureData: ByteArray, messageToValidate: ByteArray) { + val groupPubKey = groupSessionId.pubKeyBytes + if (!ED25519.verify(signature = signatureData, ed25519PublicKey = groupPubKey, message = messageToValidate)) { + throw SignatureException("Verification failed for signature data") + } + } + + private fun handleInviteResponse(message: GroupUpdated, closedGroup: AccountId) { + val sender = message.sender!! + // val profile = message // maybe we do need data to be the inner so we can access profile + val approved = message.inner.inviteResponse.isApproved + scope.launch { + try { + groupManagerV2.handleInviteResponse(closedGroup, AccountId(sender), approved) + } catch (e: Exception) { + Log.e("GroupUpdated", "Failed to handle invite response", e) + } + } + } + + + private fun handlePromotionMessage(message: GroupUpdated) { + val promotion = message.inner.promoteMessage + val seed = promotion.groupIdentitySeed.toByteArray() + val sender = message.sender!! + val adminId = AccountId(sender) + scope.launch { + try { + groupManagerV2 + .handlePromotion( + groupId = AccountId(IdPrefix.GROUP, ED25519.generate(seed).pubKey.data), + groupName = promotion.name, + adminKeySeed = seed, + promoter = adminId, + promoterName = message.profile?.displayName, + promoteMessageHash = message.serverHash!!, + promoteMessageTimestamp = message.sentTimestamp!!, + ) + } catch (e: Exception) { + Log.e("GroupUpdated", "Failed to handle promotion message", e) + } + } + } + + private fun handleGroupInfoChange(message: GroupUpdated, closedGroup: AccountId) { + val inner = message.inner + val infoChanged = inner.infoChangeMessage ?: return + if (!infoChanged.hasAdminSignature()) return Log.e("GroupUpdated", "Info changed message doesn't contain admin signature") + val adminSignature = infoChanged.adminSignature + val type = infoChanged.type + val timestamp = message.sentTimestamp!! + verifyAdminSignature(closedGroup, adminSignature.toByteArray(), buildInfoChangeSignature(type, timestamp)) + + groupManagerV2.handleGroupInfoChange(message, closedGroup) + } + + + private fun handleMemberChange(message: GroupUpdated, closedGroup: AccountId) { + val memberChange = message.inner.memberChangeMessage + val type = memberChange.type + val timestamp = message.sentTimestamp!! + verifyAdminSignature(closedGroup, + memberChange.adminSignature.toByteArray(), + buildMemberChangeSignature(type, timestamp) + ) + storage.insertGroupInfoChange(message, closedGroup) + } + + private fun handleMemberLeft(message: GroupUpdated, closedGroup: AccountId) { + scope.launch { + try { + groupManagerV2.handleMemberLeftMessage( + AccountId(message.sender!!), closedGroup + ) + } catch (e: Exception) { + Log.e("GroupUpdated", "Failed to handle member left message", e) + } + } + } + + private fun handleMemberLeftNotification(message: GroupUpdated, closedGroup: AccountId) { + storage.insertGroupInfoChange(message, closedGroup) + } + + private fun handleDeleteMemberContent(message: GroupUpdated, closedGroup: AccountId) { + val deleteMemberContent = message.inner.deleteMemberContent + val adminSig = if (deleteMemberContent.hasAdminSignature()) deleteMemberContent.adminSignature.toByteArray()!! else byteArrayOf() + + val hasValidAdminSignature = adminSig.isNotEmpty() && runCatching { + verifyAdminSignature( + closedGroup, + adminSig, + buildDeleteMemberContentSignature( + memberIds = deleteMemberContent.memberSessionIdsList.asSequence().map(::AccountId).asIterable(), + messageHashes = deleteMemberContent.messageHashesList, + timestamp = message.sentTimestamp!!, + ) + ) + }.isSuccess + + scope.launch { + try { + groupManagerV2.handleDeleteMemberContent( + groupId = closedGroup, + deleteMemberContent = deleteMemberContent, + timestamp = message.sentTimestamp!!, + sender = AccountId(message.sender!!), + senderIsVerifiedAdmin = hasValidAdminSignature + ) + } catch (e: Exception) { + Log.e("GroupUpdated", "Failed to handle delete member content", e) + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt index 2628727266..738b3c9fa7 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt @@ -1,6 +1,6 @@ package org.session.libsession.messaging.sending_receiving -import network.loki.messenger.libsession_util.protocol.DecodedEnvelop +import network.loki.messenger.libsession_util.protocol.DecodedEnvelope import network.loki.messenger.libsession_util.protocol.SessionProtocol import network.loki.messenger.libsession_util.util.BlindKeyAPI import org.session.libsession.database.StorageProtocol @@ -64,7 +64,7 @@ class MessageHandler @Inject constructor( } private fun parseMessage( - decodedEnvelop: DecodedEnvelop, + decodedEnvelope: DecodedEnvelope, relaxSignatureCheck: Boolean, checkForBlockStatus: Boolean, isForGroup: Boolean, @@ -73,9 +73,9 @@ class MessageHandler @Inject constructor( senderIdPrefix: IdPrefix ): Pair { return parseMessage( - sender = AccountId(senderIdPrefix, decodedEnvelop.senderX25519PubKey.data), - contentPlaintext = decodedEnvelop.contentPlainText.data, - messageTimestampMs = decodedEnvelop.timestamp.toEpochMilli(), + sender = AccountId(senderIdPrefix, decodedEnvelope.senderX25519PubKey.data), + contentPlaintext = decodedEnvelope.contentPlainText.data, + messageTimestampMs = decodedEnvelope.timestamp.toEpochMilli(), relaxSignatureCheck = relaxSignatureCheck, checkForBlockStatus = checkForBlockStatus, isForGroup = isForGroup, @@ -159,7 +159,7 @@ class MessageHandler @Inject constructor( ) return parseMessage( - decodedEnvelop = envelop, + decodedEnvelope = envelop, relaxSignatureCheck = false, checkForBlockStatus = true, isForGroup = false, @@ -192,7 +192,7 @@ class MessageHandler @Inject constructor( ) return parseMessage( - decodedEnvelop = decoded, + decodedEnvelope = decoded, relaxSignatureCheck = false, checkForBlockStatus = false, isForGroup = true, diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt index 0d43629fc9..248a3f634f 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt @@ -33,7 +33,7 @@ class MessageRequestResponseHandler @Inject constructor( private val blindMappingRepository: BlindMappingRepository, ) { - suspend fun handleVisibleMessage(message: VisibleMessage) { + fun handleVisibleMessage(message: VisibleMessage) { val (sender, receiver) = fetchSenderAndReceiver(message) ?: return val allBlindedAddresses = blindMappingRepository.calculateReverseMappings( @@ -61,7 +61,7 @@ class MessageRequestResponseHandler @Inject constructor( } } - suspend fun handleExplicitRequestResponseMessage(message: MessageRequestResponse) { + fun handleExplicitRequestResponseMessage(message: MessageRequestResponse) { val (sender, receiver) = fetchSenderAndReceiver(message) ?: return // Always handle explicit request response handleRequestResponse( @@ -81,8 +81,8 @@ class MessageRequestResponseHandler @Inject constructor( } } - private suspend fun fetchSenderAndReceiver(message: Message): Pair? { - val messageSender = recipientRepository.getRecipient( + private fun fetchSenderAndReceiver(message: Message): Pair? { + val messageSender = recipientRepository.getRecipientSync( requireNotNull(message.sender) { "MessageRequestResponse must have a sender" }.toAddress() @@ -92,7 +92,7 @@ class MessageRequestResponseHandler @Inject constructor( Log.e(TAG, "MessageRequestResponse sender must be a standard address, but got: ${messageSender.address.debugString}") null } else { - messageSender to recipientRepository.getRecipient( + messageSender to recipientRepository.getRecipientSync( requireNotNull(message.recipient) { "MessageRequestResponse must have a receiver" }.toAddress() diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 46ab9d7a48..ee95c516fe 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -84,6 +84,7 @@ internal fun MessageReceiver.isBlocked(publicKey: String): Boolean { return recipient?.blocked == true } +@Deprecated(replaceWith = ReplaceWith("ReceivedMessageProcessor"), message = "Use ReceivedMessageProcessor instead") @Singleton class ReceivedMessageHandler @Inject constructor( @param:ApplicationContext private val context: Context, diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt new file mode 100644 index 0000000000..28438733fe --- /dev/null +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt @@ -0,0 +1,342 @@ +package org.session.libsession.messaging.sending_receiving + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import network.loki.messenger.R +import network.loki.messenger.libsession_util.ConfigBase +import network.loki.messenger.libsession_util.util.BlindKeyAPI +import okio.withLock +import org.session.libsession.database.MessageDataProvider +import org.session.libsession.database.userAuth +import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.control.CallMessage +import org.session.libsession.messaging.messages.control.DataExtractionNotification +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.GroupUpdated +import org.session.libsession.messaging.messages.control.MessageRequestResponse +import org.session.libsession.messaging.messages.control.ReadReceipt +import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsession.messaging.messages.control.UnsendRequest +import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.open_groups.OpenGroupMessage +import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage +import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier +import org.session.libsession.messaging.utilities.WebRtcUtils +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID +import org.session.libsession.utilities.SSKEnvironment +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.UserConfigType +import org.session.libsession.utilities.recipients.MessageType +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.recipients.getType +import org.session.libsignal.protos.SignalServiceProtos +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.RecipientRepository +import org.thoughtcrime.securesms.database.Storage +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.dependencies.ManagerScope +import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.ReentrantLock +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton + +@Singleton +class ReceivedMessageProcessor @Inject constructor( + @param:ApplicationContext private val context: Context, + private val recipientRepository: RecipientRepository, + private val messageHandler: MessageHandler, + private val storage: Storage, + private val configFactory: ConfigFactoryProtocol, + private val threadDatabase: ThreadDatabase, + private val readReceiptManager: Provider, + private val typingIndicators: Provider, + private val prefs: TextSecurePreferences, + private val groupMessageHandler: Provider, + private val messageExpirationManager: Provider, + private val messageDataProvider: MessageDataProvider, + @param:ManagerScope private val scope: CoroutineScope, + private val notificationManager: MessageNotifier, + private val messageRequestResponseHandler: Provider, + private val visibleMessageHandler: Provider, +) { + private val threadMutexes = ConcurrentHashMap() + + fun createContext(): MessageProcessingContext { + return MessageProcessingContext() + } + + fun processEnvelopedMessage( + threadAddress: Address.Conversable, + message: Message, + proto: SignalServiceProtos.Content, + context: MessageProcessingContext, + runThreadUpdate: Boolean, + runProfileUpdate: Boolean, + ) = threadMutexes.getOrPut(threadAddress) { ReentrantLock() }.withLock { + // The logic to check if the message should be discarded due to being from a hidden contact. + if (threadAddress is Address.Standard && + message.sentTimestamp != null && + shouldDiscardForHiddenContact( + ctx = context, + messageTimestamp = message.sentTimestamp!!, + threadAddress = threadAddress + ) + ) { + log { "Dropping message from hidden contact ${threadAddress.debugString}" } + return@withLock + } + + // Get or create thread ID, if we aren't allowed to create it, and it doesn't exist, drop the message + val threadId = context.threadIDs[threadAddress] ?: + if (shouldCreateThread(message)) { + threadDatabase.getOrCreateThreadIdFor(threadAddress) + .also { context.threadIDs[threadAddress] = it } + } else { + threadDatabase.getThreadIdIfExistsFor(threadAddress) + .also { id -> + if (id == -1L) { + log { "Dropping message for non-existing thread ${threadAddress.debugString}" } + return@withLock + } else { + context.threadIDs[threadAddress] = id + } + } + } + + when (message) { + is ReadReceipt -> handleReadReceipt(message) + is TypingIndicator -> handleTypingIndicator(message) + is GroupUpdated -> groupMessageHandler.get().handleGroupUpdated( + message = message, + groupId = (threadAddress as? Address.Group)?.accountId + ) + is ExpirationTimerUpdate -> { + // For groupsv2, there are dedicated mechanisms for handling expiration timers, and + // we want to avoid the 1-to-1 message format which is unauthenticated in a group settings. + if (threadAddress is Address.Group) { + Log.d("MessageReceiver", "Ignoring expiration timer update for closed group") + } // also ignore it for communities since they do not support disappearing messages + else if (threadAddress is Address.Community) { + Log.d("MessageReceiver", "Ignoring expiration timer update for communities") + } else { + handleExpirationTimerUpdate(message) + } + } + is DataExtractionNotification -> handleDataExtractionNotification(message) + is UnsendRequest -> handleUnsendRequest(message) + is MessageRequestResponse -> messageRequestResponseHandler.get().handleExplicitRequestResponseMessage(message) + is VisibleMessage -> visibleMessageHandler.get().handleVisibleMessage( + message = message, + threadId = threadId, + threadAddress = threadAddress, + ctx = context, + proto = proto, + runThreadUpdate = runThreadUpdate, + runProfileUpdate = runProfileUpdate, + ) + is CallMessage -> handleCallMessage(message) + } + + } + + fun processCommunityMessage( + threadAddress: Address.Community, + message: OpenGroupMessage, + context: MessageProcessingContext + ) = threadMutexes.getOrPut(threadAddress) { ReentrantLock() }.withLock { + + } + + private fun handleReadReceipt(message: ReadReceipt) { + readReceiptManager.get().processReadReceipts( + message.sender!!, + message.timestamps!!, + message.receivedTimestamp!! + ) + } + + private fun handleTypingIndicator(message: TypingIndicator) { + when (message.kind!!) { + TypingIndicator.Kind.STARTED -> showTypingIndicatorIfNeeded(message.sender!!) + TypingIndicator.Kind.STOPPED -> hideTypingIndicatorIfNeeded(message.sender!!) + } + } + + private fun showTypingIndicatorIfNeeded(senderPublicKey: String) { + // We don't want to show other people's indicators if the toggle is off + if(!prefs.isTypingIndicatorsEnabled()) return + + val address = Address.fromSerialized(senderPublicKey) + val threadID = storage.getThreadId(address) ?: return + typingIndicators.get().didReceiveTypingStartedMessage(threadID, address, 1) + } + + private fun hideTypingIndicatorIfNeeded(senderPublicKey: String) { + val address = Address.fromSerialized(senderPublicKey) + val threadID = storage.getThreadId(address) ?: return + typingIndicators.get().didReceiveTypingStoppedMessage(threadID, address, 1, false) + } + + + /** + * Return true if this message should result in the creation of a thread. + */ + private fun shouldCreateThread(message: Message): Boolean { + return message is VisibleMessage + } + + private fun handleExpirationTimerUpdate(message: ExpirationTimerUpdate) { + messageExpirationManager.get().run { + insertExpirationTimerMessage(message) + onMessageReceived(message) + } + } + + private fun handleDataExtractionNotification(message: DataExtractionNotification) { + // We don't handle data extraction messages for groups (they shouldn't be sent, but just in case we filter them here too) + if (message.groupPublicKey != null) return + val senderPublicKey = message.sender!! + + val notification: DataExtractionNotificationInfoMessage = when(message.kind) { + is DataExtractionNotification.Kind.MediaSaved -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) + else -> return + } + storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!) + } + + fun handleUnsendRequest(message: UnsendRequest): MessageId? { + val userPublicKey = storage.getUserPublicKey() + val userAuth = storage.userAuth ?: return null + val isLegacyGroupAdmin: Boolean = message.groupPublicKey?.let { key -> + var admin = false + val groupID = doubleEncodeGroupID(key) + val group = storage.getGroup(groupID) + if(group != null) { + admin = group.admins.map { it.toString() }.contains(message.sender) + } + admin + } ?: false + + // First we need to determine the validity of the UnsendRequest + // It is valid if: + val requestIsValid = message.sender == message.author || // the sender is the author of the message + message.author == userPublicKey || // the sender is the current user + isLegacyGroupAdmin // sender is an admin of legacy group + + if (!requestIsValid) { return null } + + val timestamp = message.timestamp ?: return null + val author = message.author ?: return null + val messageToDelete = storage.getMessageByTimestamp(timestamp, author, false) ?: return null + val messageIdToDelete = messageToDelete.messageId + val messageType = messageToDelete.individualRecipient?.getType() + + // send a /delete rquest for 1on1 messages + if (messageType == MessageType.ONE_ON_ONE) { + messageDataProvider.getServerHashForMessage(messageIdToDelete)?.let { serverHash -> + scope.launch(Dispatchers.IO) { // using scope as we are slowly migrating to coroutines but we can't migrate everything at once + try { + SnodeAPI.deleteMessage(author, userAuth, listOf(serverHash)) + } catch (e: Exception) { + Log.e("Loki", "Failed to delete message", e) + } + } + } + } + + // the message is marked as deleted locally + // except for 'note to self' where the message is completely deleted + if (messageType == MessageType.NOTE_TO_SELF){ + messageDataProvider.deleteMessage(messageIdToDelete) + } else { + messageDataProvider.markMessageAsDeleted( + messageIdToDelete, + displayedMessage = context.getString(R.string.deleteMessageDeletedGlobally) + ) + } + + // delete reactions + storage.deleteReactions(messageToDelete.messageId) + + // update notification + if (!messageToDelete.isOutgoing) { + notificationManager.updateNotification(context) + } + + return messageIdToDelete + } + + private fun handleCallMessage(message: CallMessage) { + // TODO: refactor this out to persistence, just to help debug the flow and send/receive in synchronous testing + WebRtcUtils.SIGNAL_QUEUE.trySend(message) + } + + + + /** + * Return true if the contact is marked as hidden for given message timestamp. + */ + private fun shouldDiscardForHiddenContact(ctx: MessageProcessingContext, + messageTimestamp: Long, + threadAddress: Address.Standard): Boolean { + val hidden = configFactory.withUserConfigs { configs -> + configs.contacts.get(threadAddress.address)?.priority == ConfigBase.PRIORITY_HIDDEN + } + + return hidden && + // the message's sentTimestamp is earlier than the sentTimestamp of the last config + messageTimestamp < ctx.contactConfigTimestamp + } + + inner class MessageProcessingContext( + val recipients: HashMap = hashMapOf(), + val threadIDs: HashMap = hashMapOf(), + val currentUserBlindedKeys: HashMap> = hashMapOf(), + val currentUserPublicKey: String = requireNotNull(storage.getUserPublicKey()) { + "No current user available" + }, + ) { + val contactConfigTimestamp: Long by lazy(LazyThreadSafetyMode.NONE) { + configFactory.getConfigTimestamp(UserConfigType.CONTACTS, currentUserPublicKey) + } + + fun getThreadRecipient(threadAddress: Address.Conversable): Recipient { + return recipients.getOrPut(threadAddress) { + recipientRepository.getRecipientSync(threadAddress) + } + } + + fun getCurrentUserBlindedKeysByThread(address: Address.Conversable): List { + if (address !is Address.Community) return emptyList() + return currentUserBlindedKeys.getOrPut(address) { + BlindKeyAPI.blind15Ids( + sessionId = currentUserPublicKey, + serverPubKey = requireNotNull(storage.getOpenGroupPublicKey(address.serverUrl)) { + "No open group public key for community ${address.debugString}" + } + ) + } + } + } + + companion object { + private const val TAG = "ReceivedMessageProcessor" + + private const val DEBUG_MESSAGE_PROCESSING = true + + private inline fun log(message: () -> String) { + if (DEBUG_MESSAGE_PROCESSING) { + Log.d(TAG, message()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt new file mode 100644 index 0000000000..01e02d6fa6 --- /dev/null +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt @@ -0,0 +1,259 @@ +package org.session.libsession.messaging.sending_receiving + +import android.text.TextUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN +import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE +import network.loki.messenger.libsession_util.util.BaseCommunityInfo +import network.loki.messenger.libsession_util.util.ExpiryMode +import org.session.libsession.database.MessageDataProvider +import org.session.libsession.messaging.groups.GroupManagerV2 +import org.session.libsession.messaging.jobs.AttachmentDownloadJob +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.messages.ProfileUpdateHandler +import org.session.libsession.messaging.messages.visible.Attachment +import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment +import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview +import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Address.Companion.toAddress +import org.session.libsession.utilities.SSKEnvironment +import org.session.libsession.utilities.isGroupOrCommunity +import org.session.libsession.utilities.recipients.RecipientData +import org.session.libsession.utilities.updateContact +import org.session.libsession.utilities.upsertContact +import org.session.libsignal.protos.SignalServiceProtos +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.guava.Optional +import org.thoughtcrime.securesms.database.Storage +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.dependencies.ManagerScope +import org.thoughtcrime.securesms.pro.ProStatusManager +import javax.inject.Inject +import javax.inject.Provider + +class VisibleMessageHandler @Inject constructor( + private val storage: Storage, + private val messageRequestResponseHandler: MessageRequestResponseHandler, + @param:ManagerScope private val scope: CoroutineScope, + private val groupManagerV2: GroupManagerV2, + private val messageDataProvider: MessageDataProvider, + private val proStatusManager: ProStatusManager, + private val configFactory: ConfigFactory, + private val profileUpdateHandler: Provider, + private val attachmentDownloadJobFactory: AttachmentDownloadJob.Factory, + private val messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol, + private val typingIndicators: SSKEnvironment.TypingIndicatorsProtocol, +){ + fun handleVisibleMessage( + message: VisibleMessage, + threadId: Long, + threadAddress: Address.Conversable, + ctx: ReceivedMessageProcessor.MessageProcessingContext, + proto: SignalServiceProtos.Content, + runThreadUpdate: Boolean, + runProfileUpdate: Boolean, + ): MessageId? { + val userPublicKey = storage.getUserPublicKey() + val senderAddress = message.sender!!.toAddress() + + messageRequestResponseHandler.handleVisibleMessage(message) + + // Handle group invite response if new closed group + if (threadAddress is Address.Group && senderAddress is Address.Standard) { + scope.launch { + try { + groupManagerV2 + .handleInviteResponse( + threadAddress.accountId, + senderAddress.accountId, + approved = true + ) + } catch (e: Exception) { + Log.e("Loki", "Failed to handle invite response", e) + } + } + } + // Parse quote if needed + var quoteModel: QuoteModel? = null + var quoteMessageBody: String? = null + if (message.quote != null && proto.dataMessage.hasQuote()) { + val quote = proto.dataMessage.quote + + val author = if (quote.author in ctx.getCurrentUserBlindedKeysByThread(threadAddress)) { + Address.fromSerialized(userPublicKey!!) + } else { + Address.fromSerialized(quote.author) + } + + val messageInfo = messageDataProvider.getMessageForQuote(threadId, quote.id, author) + quoteMessageBody = messageInfo?.third + quoteModel = if (messageInfo != null) { + val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList() + QuoteModel(quote.id, author,null,false, attachments) + } else { + QuoteModel(quote.id, author,null, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList)) + } + } + // Parse link preview if needed + val linkPreviews: MutableList = mutableListOf() + if (message.linkPreview != null && proto.dataMessage.previewCount > 0) { + for (preview in proto.dataMessage.previewList) { + val thumbnail = PointerAttachment.forPointer(preview.image) + val url = Optional.fromNullable(preview.url) + val title = Optional.fromNullable(preview.title) + val hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent + if (hasContent) { + val linkPreview = LinkPreview(url.get(), title.or(""), thumbnail) + linkPreviews.add(linkPreview) + } else { + Log.w("Loki", "Discarding an invalid link preview. hasContent: $hasContent") + } + } + } + // Parse attachments if needed + val attachments = proto.dataMessage.attachmentsList.map(Attachment::fromProto).filter { it.isValid() } + + // Cancel any typing indicators if needed + cancelTypingIndicatorsIfNeeded(message.sender!!) + + // Parse reaction if needed + val threadIsGroup = threadAddress.isGroupOrCommunity + message.reaction?.let { reaction -> + if (reaction.react == true) { + reaction.serverId = message.openGroupServerMessageID?.toString() ?: message.serverHash.orEmpty() + reaction.dateSent = message.sentTimestamp ?: 0 + reaction.dateReceived = message.receivedTimestamp ?: 0 + storage.addReaction( + threadId = threadId, + reaction = reaction, + messageSender = senderAddress.address, + notifyUnread = !threadIsGroup + ) + } else { + storage.removeReaction( + emoji = reaction.emoji!!, + messageTimestamp = reaction.timestamp!!, + threadId = threadId, + author = senderAddress.address, + notifyUnread = threadIsGroup + ) + } + } ?: run { + // A user is mentioned if their public key is in the body of a message or one of their messages + // was quoted + + // Verify the incoming message length and truncate it if needed, before saving it to the db + val maxChars = proStatusManager.getIncomingMessageMaxLength(message) + val messageText = message.text?.take(maxChars) // truncate to max char limit for this message + message.text = messageText + message.hasMention = (sequenceOf(ctx.currentUserPublicKey) + ctx.getCurrentUserBlindedKeysByThread(threadAddress).asSequence()) + .any { key -> + messageText?.contains("@$key") == true || key == (quoteModel?.author?.toString() ?: "") + } + + // Persist the message + message.threadID = threadId + + // clean up the message - For example we do not want any expiration data on messages for communities + if(message.openGroupServerMessageID != null){ + message.expiryMode = ExpiryMode.NONE + } + + val threadRecipient = ctx.getThreadRecipient(threadAddress) + val messageID = storage.persist( + threadRecipient = threadRecipient, + message = message, + quotes = quoteModel, + linkPreview = linkPreviews, + attachments = attachments, + runThreadUpdate = runThreadUpdate + ) ?: return null + + // If we have previously "hidden" the sender, we should flip the flag back to visible + if (senderAddress is Address.Standard && senderAddress.address != userPublicKey) { + val existingContact = + configFactory.withUserConfigs { it.contacts.get(senderAddress.accountId.hexString) } + + if (existingContact != null && existingContact.priority == PRIORITY_HIDDEN) { + Log.d(TAG, "Flipping thread for ${senderAddress.debugString} to visible") + configFactory.withMutableUserConfigs { configs -> + configs.contacts.updateContact(senderAddress) { + priority = PRIORITY_VISIBLE + } + } + } else if (existingContact == null || !existingContact.approvedMe) { + // If we don't have the contact, create a new one with approvedMe = true + Log.d(TAG, "Creating new contact for ${senderAddress.debugString} with approvedMe = true") + configFactory.withMutableUserConfigs { configs -> + configs.contacts.upsertContact(senderAddress) { + approvedMe = true + } + } + } + } + + // Update profile if needed: + // - must be done after the message is persisted) + // - must be done after neccessary contact is created + if (runProfileUpdate && senderAddress is Address.WithAccountId) { + val updates = ProfileUpdateHandler.Updates.create( + name = message.profile?.displayName, + picUrl = message.profile?.profilePictureURL, + picKey = message.profile?.profileKey, + blocksCommunityMessageRequests = message.blocksMessageRequests, + proStatus = null, + profileUpdateTime = message.profile?.profileUpdated, + ) + + if (updates != null) { + profileUpdateHandler.get().handleProfileUpdate( + senderId = senderAddress.accountId, + updates = updates, + fromCommunity = (threadRecipient.data as? RecipientData.Community)?.let { data -> + BaseCommunityInfo(baseUrl = data.serverUrl, room = data.room, pubKeyHex = data.serverPubKey) + }, + ) + } + } + + // Parse & persist attachments + // Start attachment downloads if needed + if (messageID.mms && (threadRecipient.autoDownloadAttachments == true || senderAddress.address == userPublicKey)) { + storage.getAttachmentsForMessage(messageID.id).iterator().forEach { attachment -> + attachment.attachmentId?.let { id -> + JobQueue.shared.add( + attachmentDownloadJobFactory.create( + attachmentID = id.rowId, + mmsMessageId = messageID.id + )) + } + } + } + message.openGroupServerMessageID?.let { + storage.setOpenGroupServerMessageID( + messageID = messageID, + serverID = it, + threadID = threadId + ) + } + message.id = messageID + messageExpirationManager.onMessageReceived(message) + return messageID + } + return null + } + + private fun cancelTypingIndicatorsIfNeeded(senderPublicKey: String) { + val address = Address.fromSerialized(senderPublicKey) + val threadID = storage.getThreadId(address) ?: return + typingIndicators.didReceiveIncomingMessage(threadID, address, 1) + } + + companion object { + private const val TAG = "VisibleMessageHandler" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 3638d2f493..82349bdd4c 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -397,15 +397,17 @@ class OpenGroupPoller @AssistedInject constructor( } } - envelopes.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { list -> - val parameters = list.map { (serverId, message, reactions) -> - MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId, reactions = reactions) - } - JobQueue.shared.add(batchMessageJobFactory.create( - parameters, - fromCommunity = threadAddress - )) - } + //TODO: Re-enable community polling + +// envelopes.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { list -> +// val parameters = list.map { (serverId, message, reactions) -> +// MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId, reactions = reactions) +// } +// JobQueue.shared.add(batchMessageJobFactory.create( +// parameters, +// fromCommunity = threadAddress +// )) +// } if (envelopes.isNotEmpty()) { JobQueue.shared.add(trimThreadJobFactory.create(threadId)) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 616c0ac52a..a4d6f7a353 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -27,13 +27,15 @@ import kotlinx.coroutines.supervisorScope import network.loki.messenger.libsession_util.Namespace import org.session.libsession.database.StorageProtocol import org.session.libsession.database.userAuth -import org.session.libsession.messaging.jobs.BatchMessageReceiveJob -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.MessageReceiveParameters +import org.session.libsession.messaging.messages.Message.Companion.senderOrSync +import org.session.libsession.messaging.sending_receiving.MessageHandler +import org.session.libsession.messaging.sending_receiving.ReceivedMessageProcessor import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeClock import org.session.libsession.snode.model.RetrieveMessageResponse import org.session.libsession.snode.utilities.await +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigMessage import org.session.libsession.utilities.TextSecurePreferences @@ -42,9 +44,9 @@ import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Snode import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager import org.thoughtcrime.securesms.util.NetworkConnectivity -import java.time.Instant import kotlin.time.Duration.Companion.days private const val TAG = "Poller" @@ -58,9 +60,11 @@ class Poller @AssistedInject constructor( private val preferences: TextSecurePreferences, private val appVisibilityManager: AppVisibilityManager, private val networkConnectivity: NetworkConnectivity, - private val batchMessageReceiveJobFactory: BatchMessageReceiveJob.Factory, private val snodeClock: SnodeClock, private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, + private val processor: ReceivedMessageProcessor, + private val messageHandler: MessageHandler, + private val threadDatabase: ThreadDatabase, @Assisted scope: CoroutineScope ) { private val userPublicKey: String @@ -219,14 +223,49 @@ class Poller @AssistedInject constructor( Log.d(TAG, "Received ${messages.size} personal messages from snode") - receivedMessageHashDatabase.withRemovedDuplicateMessages( - swarmPublicKey = userPublicKey, - namespace = Namespace.DEFAULT(), - messages = messages, - messageHashGetter = { it.hash }, - ) { filtered -> - Log.d(TAG, "About to process ${filtered.size} new personal messages") + val start = System.currentTimeMillis() + + val processingContext = processor.createContext() + + for (message in messages) { + if (receivedMessageHashDatabase.checkOrUpdateDuplicateState( + swarmPublicKey = userPublicKey, + namespace = Namespace.DEFAULT(), + hash = message.hash + )) { + Log.d(TAG, "Skipping duplicated message ${message.hash}") + continue + } + + try { + val (message, proto) = messageHandler.parse1o1Message( + data = message.data, + serverHash = message.hash + ) + + processor.processEnvelopedMessage( + threadAddress = message.senderOrSync.toAddress() as Address.Conversable, + message = message, + proto = proto, + context = processingContext, + runThreadUpdate = false, + runProfileUpdate = true + ) + } catch (ec: Exception) { + Log.e( + TAG, + "Error while processing personal message with hash ${message.hash}", + ec + ) + } + } + + // Bulk update threads at the end + for (thread in processingContext.threadIDs.values) { + threadDatabase.update(thread, true) } + + Log.d(TAG, "Processed ${messages.size} personal messages in ${System.currentTimeMillis() - start} ms") } private fun processConfig(messages: List, forConfig: UserConfigType) { @@ -235,28 +274,36 @@ class Poller @AssistedInject constructor( return } - try { - receivedMessageHashDatabase.withRemovedDuplicateMessages( - swarmPublicKey = userPublicKey, - namespace = forConfig.namespace, - messages = messages, - messageHashGetter = { it.hash }, - ) { filtered -> - if (filtered.isNotEmpty()) { - configFactory.mergeUserConfigs( - userConfigType = forConfig, - messages = filtered - .mapTo(arrayListOf()) { m -> - ConfigMessage(data = m.data, hash = m.hash, timestamp = m.timestamp.toEpochMilli()) - } - ) - } + val newMessages = messages + .asSequence() + .filterNot { msg -> + receivedMessageHashDatabase.checkOrUpdateDuplicateState( + swarmPublicKey = userPublicKey, + namespace = forConfig.namespace, + hash = msg.hash + ) + } + .map { m-> + ConfigMessage( + data = m.data, + hash = m.hash, + timestamp = m.timestamp.toEpochMilli() + ) + } + .toList() - Log.d(TAG, "Completed processing ${filtered.size} messages for $forConfig") + if (newMessages.isNotEmpty()) { + try { + configFactory.mergeUserConfigs( + userConfigType = forConfig, + messages = newMessages + ) + } catch (e: Exception) { + Log.e(TAG, "Error while merging user configs for $forConfig", e) } - } catch (e: Exception) { - Log.e(TAG, "Error while merging user configs", e) } + + Log.d(TAG, "Processed ${newMessages.size} new messages for config $forConfig") } @@ -378,12 +425,11 @@ class Poller @AssistedInject constructor( val messages = result.getOrThrow().messages processPersonalMessages(messages) - if (messages.isNotEmpty()) { + messages.maxByOrNull { it.timestamp }?.let { newest -> lokiApiDatabase.setLastMessageHashValue( snode = snode, publicKey = userPublicKey, - newValue = messages - .maxBy { it.timestamp }.hash, + newValue = newest.hash, namespace = Namespace.DEFAULT() ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt index 5dcc41a20a..6494b1e50d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt @@ -1,12 +1,11 @@ package org.thoughtcrime.securesms.database import android.content.Context +import androidx.collection.LruCache import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.transaction import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.serialization.json.Json import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.util.asSequence import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton @@ -17,7 +16,16 @@ class ReceivedMessageHashDatabase @Inject constructor( databaseHelper: Provider, private val json: Json, ) : Database(context, databaseHelper) { + + private data class CacheKey(val publicKey: String, val namespace: Int, val hash: String) + + private val cache = LruCache(1024) + fun removeAllByNamespaces(vararg namespaces: Int) { + synchronized(cache) { + cache.evictAll() + } + //language=roomsql writableDatabase.rawExecSQL(""" DELETE FROM received_messages @@ -26,6 +34,10 @@ class ReceivedMessageHashDatabase @Inject constructor( } fun removeAllByPublicKey(publicKey: String) { + synchronized(cache) { + cache.evictAll() + } + //language=roomsql writableDatabase.rawExecSQL(""" DELETE FROM received_messages @@ -34,71 +46,50 @@ class ReceivedMessageHashDatabase @Inject constructor( } fun removeAll() { - //language=roomsql - writableDatabase.rawExecSQL("DELETE FROM received_messages WHERE 1") - } - - private fun removeDuplicatedHashes( - swarmPublicKey: String, - namespace: Int, - hashes: Collection - ): Set { - val hashesAsJsonText = json.encodeToString(hashes) + synchronized(cache) { + cache.evictAll() + } //language=roomsql - return writableDatabase.rawQuery(""" - SELECT ea.value FROM json_each(?) ea - WHERE NOT EXISTS ( - SELECT 1 FROM received_messages m - WHERE m.swarm_pub_key = ? AND m.namespace = ? AND m.hash = ea.value - ) - """, hashesAsJsonText, swarmPublicKey, namespace).use { cursor -> - cursor.asSequence() - .mapTo(hashSetOf()) { it.getString(0) } - } + writableDatabase.rawExecSQL("DELETE FROM received_messages WHERE 1") } /** - * Filters out messages with duplicate hashes from the provided list, performs the given action - * on the new messages, and then adds the new message hashes to the database. + * Checks if the given [hash] is already present in the database for the given + * [swarmPublicKey] and [namespace]. If not, adds it to the database. + * + * This implementation is atomic. + * + * @return true if the hash was already in the db */ - fun withRemovedDuplicateMessages( + fun checkOrUpdateDuplicateState( swarmPublicKey: String, namespace: Int, - messages: List, - messageHashGetter: (M) -> String, - performOnNewMessages: (List) -> T, - ): T { - return writableDatabase.transaction { - val newMessageHashes = removeDuplicatedHashes( - swarmPublicKey = swarmPublicKey, - namespace = namespace, - hashes = messages.map(messageHashGetter) - ) - - val ret = performOnNewMessages( - messages.filter { m -> messageHashGetter(m) in newMessageHashes } - ) - - addHashes( - swarmPublicKey = swarmPublicKey, - namespace = namespace, - hashes = newMessageHashes - ) - - ret + hash: String + ): Boolean { + val key = CacheKey(swarmPublicKey, namespace, hash) + synchronized(cache) { + if (cache[key] != null) { + return true + } } - } - private fun addHashes(swarmPublicKey: String, namespace: Int, hashes: Collection) { //language=roomsql - writableDatabase.rawExecSQL(""" + return writableDatabase.compileStatement(""" INSERT OR IGNORE INTO received_messages (swarm_pub_key, namespace, hash) - SELECT ?, ?, value FROM json_each(?) - """, swarmPublicKey, namespace, json.encodeToString(hashes)) + VALUES (?, ?, ?) + """).use { stmt -> + stmt.bindString(1, swarmPublicKey) + stmt.bindLong(2, namespace.toLong()) + stmt.bindString(3, hash) + stmt.executeUpdateDelete() == 0 + }.also { + synchronized(cache) { + cache.put(key, Unit) + } + } } - companion object { fun createAndMigrateTable(db: SupportSQLiteDatabase) { //language=roomsql diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index 0c35213937..96461d14ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -20,11 +20,13 @@ import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import network.loki.messenger.libsession_util.Namespace -import org.session.libsession.messaging.jobs.BatchMessageReceiveJob +import org.session.libsession.messaging.sending_receiving.MessageHandler +import org.session.libsession.messaging.sending_receiving.ReceivedMessageProcessor import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeClock import org.session.libsession.snode.model.BatchResponse import org.session.libsession.snode.model.RetrieveMessageResponse +import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigMessage import org.session.libsession.utilities.getGroup @@ -34,6 +36,7 @@ import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Snode import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager import org.thoughtcrime.securesms.util.getRootCause import java.time.Instant @@ -49,8 +52,10 @@ class GroupPoller @AssistedInject constructor( private val clock: SnodeClock, private val appVisibilityManager: AppVisibilityManager, private val groupRevokedMessageHandler: GroupRevokedMessageHandler, - private val batchMessageReceiveJobFactory: BatchMessageReceiveJob.Factory, private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, + private val messageHandler: MessageHandler, + private val receivedMessageProcessor: ReceivedMessageProcessor, + private val threadDatabase: ThreadDatabase, ) { companion object { private const val POLL_INTERVAL = 3_000L @@ -326,11 +331,11 @@ class GroupPoller @AssistedInject constructor( val regularMessages = groupMessageRetrieval.await() handleMessages(regularMessages.messages) - if (regularMessages.messages.isNotEmpty()) { + regularMessages.messages.maxByOrNull { it.timestamp }?.let { newest -> lokiApiDatabase.setLastMessageHashValue( snode = snode, publicKey = groupId.hexString, - newValue = regularMessages.messages.maxBy { it.timestamp }.hash, + newValue = newest.hash, namespace = Namespace.GROUP_MESSAGES() ) } @@ -440,20 +445,54 @@ class GroupPoller @AssistedInject constructor( ) } + /** + * @return The newest message handled, or null if no new messages were handled + */ private fun handleMessages(messages: List) { if (messages.isEmpty()) { return } - Log.d(TAG, "${groupId}: Received ${messages.size} group messages from snode") - receivedMessageHashDatabase.withRemovedDuplicateMessages( - swarmPublicKey = groupId.hexString, - namespace = Namespace.GROUP_MESSAGES(), - messages = messages, - messageHashGetter = { it.hash }, - ) { newMessages -> - Log.d(TAG, "${groupId}: Handling ${newMessages.size} new group messages") + val start = System.currentTimeMillis() + val threadAddress = Address.Group(groupId) + val processingContext = receivedMessageProcessor.createContext() + + for (message in messages) { + if (receivedMessageHashDatabase.checkOrUpdateDuplicateState( + swarmPublicKey = groupId.hexString, + namespace = Namespace.GROUP_MESSAGES(), + hash = message.hash + )) { + Log.v(TAG, "Skipping duplicated group message ${message.hash} for group $groupId") + continue + } + + try { + val (msg, proto) = messageHandler.parseGroupMessage( + data = message.data, + serverHash = message.hash, + groupId = groupId, + ) + + receivedMessageProcessor.processEnvelopedMessage( + threadAddress = threadAddress, + message = msg, + proto = proto, + context = processingContext, + runThreadUpdate = false, + runProfileUpdate = true + ) + } catch (e: Exception) { + Log.e(TAG, "Error handling group message", e) + } + } + + // Update threads after processing all messages + processingContext.threadIDs.values.forEach { threadId -> + threadDatabase.update(threadId, true) } + + Log.d(TAG, "Handled ${messages.size} group messages for $groupId in ${System.currentTimeMillis() - start}ms") } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt index 8c34519359..e288ce24df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -10,31 +10,32 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat.getString import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import network.loki.messenger.R import network.loki.messenger.libsession_util.Namespace import network.loki.messenger.libsession_util.SessionEncrypt import okio.ByteString.Companion.decodeHex -import org.session.libsession.messaging.jobs.BatchMessageReceiveJob -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.MessageReceiveParameters -import org.session.libsession.messaging.messages.Destination +import org.session.libsession.messaging.messages.Message.Companion.senderOrSync +import org.session.libsession.messaging.sending_receiving.MessageHandler +import org.session.libsession.messaging.sending_receiving.ReceivedMessageProcessor import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata -import org.session.libsession.messaging.utilities.MessageWrapper +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigMessage import org.session.libsession.utilities.bencode.Bencode import org.session.libsession.utilities.bencode.BencodeList import org.session.libsession.utilities.bencode.BencodeString import org.session.libsession.utilities.getGroup -import org.session.libsignal.protos.SignalServiceProtos.Envelope import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.groups.GroupRevokedMessageHandler import org.thoughtcrime.securesms.home.HomeActivity import java.security.SecureRandom @@ -47,7 +48,10 @@ class PushReceiver @Inject constructor( private val configFactory: ConfigFactory, private val groupRevokedMessageHandler: GroupRevokedMessageHandler, private val json: Json, - private val batchJobFactory: BatchMessageReceiveJob.Factory, + private val messageHandler: MessageHandler, + private val receivedMessageProcessor: ReceivedMessageProcessor, + private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, + @param:ManagerScope private val scope: CoroutineScope, ) { /** @@ -70,7 +74,7 @@ class PushReceiver @Inject constructor( private fun addMessageReceiveJob(pushData: PushData?) { try { val namespace = pushData?.metadata?.namespace - val params = when { + when { namespace == Namespace.GROUP_MESSAGES() || namespace == Namespace.REVOKED_GROUP_MESSAGES() || namespace == Namespace.GROUP_INFO() || @@ -91,53 +95,66 @@ class PushReceiver @Inject constructor( return } - if (namespace == Namespace.GROUP_MESSAGES()) { - val envelope = checkNotNull(tryDecryptGroupEnvelope(groupId, pushData.data)) { - "Unable to decrypt closed group message" + when (namespace) { + Namespace.GROUP_MESSAGES() -> { + if (!receivedMessageHashDatabase.checkOrUpdateDuplicateState( + swarmPublicKey = groupId.hexString, + namespace = namespace, + hash = pushData.metadata.msg_hash + )) { + val (msg, proto) = messageHandler.parseGroupMessage( + data = pushData.data, + serverHash = pushData.metadata.msg_hash, + groupId = groupId + ) + + receivedMessageProcessor.processEnvelopedMessage( + threadAddress = Address.Group(groupId), + message = msg, + proto = proto, + context = receivedMessageProcessor.createContext(), + runThreadUpdate = true, + runProfileUpdate = true, + ) + } } - MessageReceiveParameters( - data = envelope.toByteArray(), - serverHash = pushData.metadata.msg_hash, - closedGroup = Destination.ClosedGroup(groupId.hexString) - ) - } else if (namespace == Namespace.REVOKED_GROUP_MESSAGES()) { - GlobalScope.launch { - groupRevokedMessageHandler.handleRevokeMessage(groupId, listOf(pushData.data)) + Namespace.REVOKED_GROUP_MESSAGES() -> { + scope.launch { + groupRevokedMessageHandler.handleRevokeMessage(groupId, listOf(pushData.data)) + } } - null - } else { - val hash = requireNotNull(pushData.metadata.msg_hash) { - "Received a closed group config push notification without a message hash" - } - - // If we receive group config messages from notification, try to merge - // them directly - val configMessage = listOf( - ConfigMessage( - hash = hash, - data = pushData.data, - timestamp = pushData.metadata.timestampSeconds + else -> { + val hash = requireNotNull(pushData.metadata.msg_hash) { + "Received a closed group config push notification without a message hash" + } + + // If we receive group config messages from notification, try to merge + // them directly + val configMessage = listOf( + ConfigMessage( + hash = hash, + data = pushData.data, + timestamp = pushData.metadata.timestampSeconds + ) ) - ) - configFactory.mergeGroupConfigMessages( - groupId = groupId, - keys = configMessage.takeIf { namespace == Namespace.GROUP_KEYS() } - .orEmpty(), - members = configMessage.takeIf { namespace == Namespace.GROUP_MEMBERS() } - .orEmpty(), - info = configMessage.takeIf { namespace == Namespace.GROUP_INFO() } - .orEmpty(), - ) - - null + configFactory.mergeGroupConfigMessages( + groupId = groupId, + keys = configMessage.takeIf { namespace == Namespace.GROUP_KEYS() } + .orEmpty(), + members = configMessage.takeIf { namespace == Namespace.GROUP_MEMBERS() } + .orEmpty(), + info = configMessage.takeIf { namespace == Namespace.GROUP_INFO() } + .orEmpty(), + ) + } } } namespace == Namespace.DEFAULT() || pushData?.metadata == null -> { - if (pushData?.data == null) { + if (pushData?.data == null || pushData.metadata?.msg_hash == null) { Log.d(TAG, "Push data is null") if(pushData?.metadata?.data_too_long != true) { Log.d(TAG, "Sending a generic notification (data_too_long was false)") @@ -146,11 +163,25 @@ class PushReceiver @Inject constructor( return } - val envelopeAsData = MessageWrapper.unwrap(pushData.data).toByteArray() - MessageReceiveParameters( - data = envelopeAsData, - serverHash = pushData.metadata?.msg_hash - ) + if (!receivedMessageHashDatabase.checkOrUpdateDuplicateState( + swarmPublicKey = pushData.metadata.account, + namespace = Namespace.DEFAULT(), + hash = pushData.metadata.msg_hash + )) { + val (message, proto) = messageHandler.parse1o1Message( + data = pushData.data, + serverHash = pushData.metadata.msg_hash, + ) + + receivedMessageProcessor.processEnvelopedMessage( + threadAddress = message.senderOrSync.toAddress() as Address.Conversable, + message = message, + proto = proto, + context = receivedMessageProcessor.createContext(), + runThreadUpdate = true, + runProfileUpdate = true, + ) + } } else -> { @@ -159,33 +190,12 @@ class PushReceiver @Inject constructor( } } - if (params != null) { - JobQueue.shared.add(batchJobFactory.create( - messages = listOf(params), - fromCommunity = null - )) - } } catch (e: Exception) { Log.d(TAG, "Failed to unwrap data for message due to error.", e) } } - private fun tryDecryptGroupEnvelope(groupId: AccountId, data: ByteArray): Envelope? { - val (envelopBytes, sender) = checkNotNull(configFactory.withGroupConfigs(groupId) { - it.groupKeys.decrypt( - data - ) - }) { - "Failed to decrypt group message" - } - - Log.d(TAG, "Successfully decrypted group message from $sender") - return Envelope.parseFrom(envelopBytes) - .toBuilder() - .setSource(sender) - .build() - } private fun sendGenericNotification() { // no need to do anything if notification permissions are not granted @@ -265,7 +275,7 @@ class PushReceiver @Inject constructor( return key } - data class PushData( + class PushData( val data: ByteArray?, val metadata: PushNotificationMetadata? ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81df3b7393..375726b33b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ kotlinVersion = "2.2.20" kryoVersion = "5.6.2" kspVersion = "2.3.0" legacySupportV13Version = "1.0.0" -libsessionUtilAndroidVersion = "1.0.9-14-g80f501d" +libsessionUtilAndroidVersion = "1.0.9-17-g290907f" media3ExoplayerVersion = "1.8.0" mockitoCoreVersion = "5.20.0" navVersion = "2.9.5" From fbf08a8417c9b3d7baba0c02bdc87fa1bccc84ea Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:41:12 +1100 Subject: [PATCH 13/19] Poll handling --- .../{MessageHandler.kt => MessageParser.kt} | 2 +- .../ReceivedMessageProcessor.kt | 58 +++++++++++----- .../VisibleMessageHandler.kt | 7 +- .../sending_receiving/pollers/Poller.kt | 67 +++++++++---------- .../securesms/groups/GroupPoller.kt | 64 ++++++++---------- .../securesms/notifications/PushReceiver.kt | 40 +++++------ gradle/libs.versions.toml | 2 +- 7 files changed, 127 insertions(+), 113 deletions(-) rename app/src/main/java/org/session/libsession/messaging/sending_receiving/{MessageHandler.kt => MessageParser.kt} (99%) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt similarity index 99% rename from app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt rename to app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt index 738b3c9fa7..8dd3adae49 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt @@ -29,7 +29,7 @@ import javax.inject.Singleton import kotlin.math.abs @Singleton -class MessageHandler @Inject constructor( +class MessageParser @Inject constructor( private val configFactory: ConfigFactoryProtocol, private val storage: StorageProtocol, private val snodeClock: SnodeClock, diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt index 28438733fe..5857cd779c 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt @@ -53,7 +53,7 @@ import javax.inject.Singleton class ReceivedMessageProcessor @Inject constructor( @param:ApplicationContext private val context: Context, private val recipientRepository: RecipientRepository, - private val messageHandler: MessageHandler, + private val messageParser: MessageParser, private val storage: Storage, private val configFactory: ConfigFactoryProtocol, private val threadDatabase: ThreadDatabase, @@ -70,17 +70,35 @@ class ReceivedMessageProcessor @Inject constructor( ) { private val threadMutexes = ConcurrentHashMap() - fun createContext(): MessageProcessingContext { - return MessageProcessingContext() + + /** + * Start a message processing session, ensuring that thread updates and notifications are handled + * once the whole processing is complete. + * + * Note: the context passed to the block is not thread-safe, so it should not be shared between threads. + */ + fun startProcessing(block: (MessageProcessingContext) -> T): T { + val context = MessageProcessingContext() + try { + return block(context) + } finally { + for (threadId in context.threadIDs.values) { + if (context.maxOutgoingMessageTimestamp > 0L && + context.maxOutgoingMessageTimestamp > storage.getLastSeen(threadId)) { + storage.markConversationAsRead(threadId, context.maxOutgoingMessageTimestamp, force = true) + } + + storage.updateThread(threadId, true) + notificationManager.updateNotification(this.context, threadId) + } + } } fun processEnvelopedMessage( + context: MessageProcessingContext, threadAddress: Address.Conversable, message: Message, proto: SignalServiceProtos.Content, - context: MessageProcessingContext, - runThreadUpdate: Boolean, - runProfileUpdate: Boolean, ) = threadMutexes.getOrPut(threadAddress) { ReentrantLock() }.withLock { // The logic to check if the message should be discarded due to being from a hidden contact. if (threadAddress is Address.Standard && @@ -134,15 +152,24 @@ class ReceivedMessageProcessor @Inject constructor( is DataExtractionNotification -> handleDataExtractionNotification(message) is UnsendRequest -> handleUnsendRequest(message) is MessageRequestResponse -> messageRequestResponseHandler.get().handleExplicitRequestResponseMessage(message) - is VisibleMessage -> visibleMessageHandler.get().handleVisibleMessage( - message = message, - threadId = threadId, - threadAddress = threadAddress, - ctx = context, - proto = proto, - runThreadUpdate = runThreadUpdate, - runProfileUpdate = runProfileUpdate, - ) + is VisibleMessage -> { + if (message.isSenderSelf && + message.sentTimestamp != null && + message.sentTimestamp!! > context.maxOutgoingMessageTimestamp) { + context.maxOutgoingMessageTimestamp = message.sentTimestamp!! + } + + visibleMessageHandler.get().handleVisibleMessage( + message = message, + threadId = threadId, + threadAddress = threadAddress, + ctx = context, + proto = proto, + runThreadUpdate = false, + runProfileUpdate = true, + ) + } + is CallMessage -> handleCallMessage(message) } @@ -304,6 +331,7 @@ class ReceivedMessageProcessor @Inject constructor( val currentUserPublicKey: String = requireNotNull(storage.getUserPublicKey()) { "No current user available" }, + var maxOutgoingMessageTimestamp: Long = 0L, ) { val contactConfigTimestamp: Long by lazy(LazyThreadSafetyMode.NONE) { configFactory.getConfigTimestamp(UserConfigType.CONTACTS, currentUserPublicKey) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt index 01e02d6fa6..ae6516700d 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt @@ -57,7 +57,6 @@ class VisibleMessageHandler @Inject constructor( runThreadUpdate: Boolean, runProfileUpdate: Boolean, ): MessageId? { - val userPublicKey = storage.getUserPublicKey() val senderAddress = message.sender!!.toAddress() messageRequestResponseHandler.handleVisibleMessage(message) @@ -84,7 +83,7 @@ class VisibleMessageHandler @Inject constructor( val quote = proto.dataMessage.quote val author = if (quote.author in ctx.getCurrentUserBlindedKeysByThread(threadAddress)) { - Address.fromSerialized(userPublicKey!!) + Address.fromSerialized(ctx.currentUserPublicKey) } else { Address.fromSerialized(quote.author) } @@ -174,7 +173,7 @@ class VisibleMessageHandler @Inject constructor( ) ?: return null // If we have previously "hidden" the sender, we should flip the flag back to visible - if (senderAddress is Address.Standard && senderAddress.address != userPublicKey) { + if (senderAddress is Address.Standard && senderAddress.address != ctx.currentUserPublicKey) { val existingContact = configFactory.withUserConfigs { it.contacts.get(senderAddress.accountId.hexString) } @@ -222,7 +221,7 @@ class VisibleMessageHandler @Inject constructor( // Parse & persist attachments // Start attachment downloads if needed - if (messageID.mms && (threadRecipient.autoDownloadAttachments == true || senderAddress.address == userPublicKey)) { + if (messageID.mms && (threadRecipient.autoDownloadAttachments == true || senderAddress.address == ctx.currentUserPublicKey)) { storage.getAttachmentsForMessage(messageID.id).iterator().forEach { attachment -> attachment.attachmentId?.let { id -> JobQueue.shared.add( diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index a4d6f7a353..bd00e2e2e7 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -28,7 +28,7 @@ import network.loki.messenger.libsession_util.Namespace import org.session.libsession.database.StorageProtocol import org.session.libsession.database.userAuth import org.session.libsession.messaging.messages.Message.Companion.senderOrSync -import org.session.libsession.messaging.sending_receiving.MessageHandler +import org.session.libsession.messaging.sending_receiving.MessageParser import org.session.libsession.messaging.sending_receiving.ReceivedMessageProcessor import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeClock @@ -63,7 +63,7 @@ class Poller @AssistedInject constructor( private val snodeClock: SnodeClock, private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, private val processor: ReceivedMessageProcessor, - private val messageHandler: MessageHandler, + private val messageParser: MessageParser, private val threadDatabase: ThreadDatabase, @Assisted scope: CoroutineScope ) { @@ -225,46 +225,39 @@ class Poller @AssistedInject constructor( val start = System.currentTimeMillis() - val processingContext = processor.createContext() - - for (message in messages) { - if (receivedMessageHashDatabase.checkOrUpdateDuplicateState( - swarmPublicKey = userPublicKey, - namespace = Namespace.DEFAULT(), - hash = message.hash - )) { - Log.d(TAG, "Skipping duplicated message ${message.hash}") - continue - } + processor.startProcessing { ctx -> + for (message in messages) { + if (receivedMessageHashDatabase.checkOrUpdateDuplicateState( + swarmPublicKey = userPublicKey, + namespace = Namespace.DEFAULT(), + hash = message.hash + )) { + Log.d(TAG, "Skipping duplicated message ${message.hash}") + continue + } - try { - val (message, proto) = messageHandler.parse1o1Message( - data = message.data, - serverHash = message.hash - ) + try { + val (message, proto) = messageParser.parse1o1Message( + data = message.data, + serverHash = message.hash + ) - processor.processEnvelopedMessage( - threadAddress = message.senderOrSync.toAddress() as Address.Conversable, - message = message, - proto = proto, - context = processingContext, - runThreadUpdate = false, - runProfileUpdate = true - ) - } catch (ec: Exception) { - Log.e( - TAG, - "Error while processing personal message with hash ${message.hash}", - ec - ) + processor.processEnvelopedMessage( + threadAddress = message.senderOrSync.toAddress() as Address.Conversable, + message = message, + proto = proto, + context = ctx, + ) + } catch (ec: Exception) { + Log.e( + TAG, + "Error while processing personal message with hash ${message.hash}", + ec + ) + } } } - // Bulk update threads at the end - for (thread in processingContext.threadIDs.values) { - threadDatabase.update(thread, true) - } - Log.d(TAG, "Processed ${messages.size} personal messages in ${System.currentTimeMillis() - start} ms") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index 1dcee228bb..40da70daa3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import network.loki.messenger.libsession_util.Namespace -import org.session.libsession.messaging.sending_receiving.MessageHandler +import org.session.libsession.messaging.sending_receiving.MessageParser import org.session.libsession.messaging.sending_receiving.ReceivedMessageProcessor import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeClock @@ -53,7 +53,7 @@ class GroupPoller @AssistedInject constructor( private val appVisibilityManager: AppVisibilityManager, private val groupRevokedMessageHandler: GroupRevokedMessageHandler, private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, - private val messageHandler: MessageHandler, + private val messageParser: MessageParser, private val receivedMessageProcessor: ReceivedMessageProcessor, private val threadDatabase: ThreadDatabase, ) { @@ -466,41 +466,35 @@ class GroupPoller @AssistedInject constructor( val start = System.currentTimeMillis() val threadAddress = Address.Group(groupId) - val processingContext = receivedMessageProcessor.createContext() - - for (message in messages) { - if (receivedMessageHashDatabase.checkOrUpdateDuplicateState( - swarmPublicKey = groupId.hexString, - namespace = Namespace.GROUP_MESSAGES(), - hash = message.hash - )) { - Log.v(TAG, "Skipping duplicated group message ${message.hash} for group $groupId") - continue - } - try { - val (msg, proto) = messageHandler.parseGroupMessage( - data = message.data, - serverHash = message.hash, - groupId = groupId, - ) - - receivedMessageProcessor.processEnvelopedMessage( - threadAddress = threadAddress, - message = msg, - proto = proto, - context = processingContext, - runThreadUpdate = false, - runProfileUpdate = true - ) - } catch (e: Exception) { - Log.e(TAG, "Error handling group message", e) - } - } + receivedMessageProcessor.startProcessing { ctx -> + for (message in messages) { + if (receivedMessageHashDatabase.checkOrUpdateDuplicateState( + swarmPublicKey = groupId.hexString, + namespace = Namespace.GROUP_MESSAGES(), + hash = message.hash + )) { + Log.v(TAG, "Skipping duplicated group message ${message.hash} for group $groupId") + continue + } - // Update threads after processing all messages - processingContext.threadIDs.values.forEach { threadId -> - threadDatabase.update(threadId, true) + try { + val (msg, proto) = messageParser.parseGroupMessage( + data = message.data, + serverHash = message.hash, + groupId = groupId, + ) + + receivedMessageProcessor.processEnvelopedMessage( + threadAddress = threadAddress, + message = msg, + proto = proto, + context = ctx, + ) + } catch (e: Exception) { + Log.e(TAG, "Error handling group message", e) + } + } } Log.d(TAG, "Handled ${messages.size} group messages for $groupId in ${System.currentTimeMillis() - start}ms") diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt index e288ce24df..a78074137e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -18,7 +18,7 @@ import network.loki.messenger.libsession_util.Namespace import network.loki.messenger.libsession_util.SessionEncrypt import okio.ByteString.Companion.decodeHex import org.session.libsession.messaging.messages.Message.Companion.senderOrSync -import org.session.libsession.messaging.sending_receiving.MessageHandler +import org.session.libsession.messaging.sending_receiving.MessageParser import org.session.libsession.messaging.sending_receiving.ReceivedMessageProcessor import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata import org.session.libsession.utilities.Address @@ -48,7 +48,7 @@ class PushReceiver @Inject constructor( private val configFactory: ConfigFactory, private val groupRevokedMessageHandler: GroupRevokedMessageHandler, private val json: Json, - private val messageHandler: MessageHandler, + private val messageParser: MessageParser, private val receivedMessageProcessor: ReceivedMessageProcessor, private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, @param:ManagerScope private val scope: CoroutineScope, @@ -102,20 +102,20 @@ class PushReceiver @Inject constructor( namespace = namespace, hash = pushData.metadata.msg_hash )) { - val (msg, proto) = messageHandler.parseGroupMessage( + val (msg, proto) = messageParser.parseGroupMessage( data = pushData.data, serverHash = pushData.metadata.msg_hash, groupId = groupId ) - receivedMessageProcessor.processEnvelopedMessage( - threadAddress = Address.Group(groupId), - message = msg, - proto = proto, - context = receivedMessageProcessor.createContext(), - runThreadUpdate = true, - runProfileUpdate = true, - ) + receivedMessageProcessor.startProcessing { ctx -> + receivedMessageProcessor.processEnvelopedMessage( + threadAddress = Address.Group(groupId), + message = msg, + proto = proto, + context = ctx, + ) + } } } @@ -168,19 +168,19 @@ class PushReceiver @Inject constructor( namespace = Namespace.DEFAULT(), hash = pushData.metadata.msg_hash )) { - val (message, proto) = messageHandler.parse1o1Message( + val (message, proto) = messageParser.parse1o1Message( data = pushData.data, serverHash = pushData.metadata.msg_hash, ) - receivedMessageProcessor.processEnvelopedMessage( - threadAddress = message.senderOrSync.toAddress() as Address.Conversable, - message = message, - proto = proto, - context = receivedMessageProcessor.createContext(), - runThreadUpdate = true, - runProfileUpdate = true, - ) + receivedMessageProcessor.startProcessing { ctx -> + receivedMessageProcessor.processEnvelopedMessage( + threadAddress = message.senderOrSync.toAddress() as Address.Conversable, + message = message, + proto = proto, + context = ctx, + ) + } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0dba0bf15a..7a3af8339e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ kotlinVersion = "2.2.20" kryoVersion = "5.6.2" kspVersion = "2.3.0" legacySupportV13Version = "1.0.0" -libsessionUtilAndroidVersion = "1.0.9-17-g290907f" +libsessionUtilAndroidVersion = "1.0.9-21-g4fb53ed" media3ExoplayerVersion = "1.8.0" mockitoCoreVersion = "5.20.0" navVersion = "2.9.5" From 6718492bc444375a79d03fcf54fe2f3d26ca97b7 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:45:54 +1100 Subject: [PATCH 14/19] Optimize message processing --- .../sending_receiving/MessageParser.kt | 16 +++--- .../MessageRequestResponseHandler.kt | 23 ++++++-- .../ReceivedMessageHandler.kt | 4 +- .../ReceivedMessageProcessor.kt | 56 ++++++++++++++----- .../VisibleMessageHandler.kt | 4 +- .../sending_receiving/pollers/Poller.kt | 18 +++++- .../securesms/groups/GroupPoller.kt | 2 + .../securesms/notifications/PushReceiver.kt | 26 +++++---- 8 files changed, 104 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt index 8dd3adae49..8165e7099c 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt @@ -148,11 +148,11 @@ class MessageParser @Inject constructor( fun parse1o1Message( data: ByteArray, serverHash: String, + currentUserEd25519PrivKey: ByteArray, + currentUserId: AccountId, ): Pair { val envelop = SessionProtocol.decodeFor1o1( - myEd25519PrivKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.data) { - "Couldn't find current user's key" - }, + myEd25519PrivKey = currentUserEd25519PrivKey, payload = data, nowEpochMs = snodeClock.currentTimeMills(), proBackendPubKey = proBackendKey, @@ -164,7 +164,7 @@ class MessageParser @Inject constructor( checkForBlockStatus = true, isForGroup = false, senderIdPrefix = IdPrefix.STANDARD, - currentUserId = AccountId(storage.getUserPublicKey()!!), + currentUserId = currentUserId, alternativeCurrentUserIds = emptyList(), ).also { (message, _) -> message.serverHash = serverHash @@ -175,6 +175,8 @@ class MessageParser @Inject constructor( data: ByteArray, serverHash: String, groupId: AccountId, + currentUserEd25519PrivKey: ByteArray, + currentUserId: AccountId, ): Pair { val keys = configFactory.withGroupConfigs(groupId) { it.groupKeys.groupKeys() @@ -182,9 +184,7 @@ class MessageParser @Inject constructor( val decoded = SessionProtocol.decodeForGroup( payload = data, - myEd25519PrivKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.data) { - "Couldn't find current user's key" - }, + myEd25519PrivKey = currentUserEd25519PrivKey, nowEpochMs = snodeClock.currentTimeMills(), groupEd25519PublicKey = groupId.pubKeyBytes, groupEd25519PrivateKeys = keys.toTypedArray(), @@ -197,7 +197,7 @@ class MessageParser @Inject constructor( checkForBlockStatus = false, isForGroup = true, senderIdPrefix = IdPrefix.STANDARD, - currentUserId = AccountId(storage.getUserPublicKey()!!), + currentUserId = currentUserId, alternativeCurrentUserIds = emptyList(), ).also { (message, _) -> message.serverHash = serverHash diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt index 248a3f634f..d153a1606a 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt @@ -33,12 +33,16 @@ class MessageRequestResponseHandler @Inject constructor( private val blindMappingRepository: BlindMappingRepository, ) { - fun handleVisibleMessage(message: VisibleMessage) { + fun handleVisibleMessage( + ctx: ReceivedMessageProcessor.MessageProcessingContext?, + message: VisibleMessage + ) { val (sender, receiver) = fetchSenderAndReceiver(message) ?: return - val allBlindedAddresses = blindMappingRepository.calculateReverseMappings( - contactAddress = sender.address as Address.Standard - ) + val senderAddress = sender.address as Address.Standard + + val allBlindedAddresses = ctx?.getBlindIDMapping(senderAddress) + ?: blindMappingRepository.calculateReverseMappings(senderAddress) // Do we have an existing message request (including blinded requests)? val hasMessageRequest = configFactory.withUserConfigs { configs -> @@ -54,6 +58,7 @@ class MessageRequestResponseHandler @Inject constructor( if (hasMessageRequest) { handleRequestResponse( + ctx = ctx, messageSender = sender, messageReceiver = receiver, messageTimestampMs = message.sentTimestamp!!, @@ -61,10 +66,14 @@ class MessageRequestResponseHandler @Inject constructor( } } - fun handleExplicitRequestResponseMessage(message: MessageRequestResponse) { + fun handleExplicitRequestResponseMessage( + ctx: ReceivedMessageProcessor.MessageProcessingContext?, + message: MessageRequestResponse + ) { val (sender, receiver) = fetchSenderAndReceiver(message) ?: return // Always handle explicit request response handleRequestResponse( + ctx = ctx, messageSender = sender, messageReceiver = receiver, messageTimestampMs = message.sentTimestamp!!, @@ -101,6 +110,7 @@ class MessageRequestResponseHandler @Inject constructor( } private fun handleRequestResponse( + ctx: ReceivedMessageProcessor.MessageProcessingContext?, messageSender: Recipient, messageReceiver: Recipient, messageTimestampMs: Long, @@ -164,7 +174,8 @@ class MessageRequestResponseHandler @Inject constructor( // Find all blinded conversations we have with this sender, move all the messages // from the blinded conversations to the standard conversation. - val blindedConversationAddresses = blindMappingRepository.calculateReverseMappings(messageSender.address) + val blindedConversationAddresses = (ctx?.getBlindIDMapping(messageSender.address) + ?: blindMappingRepository.calculateReverseMappings(messageSender.address)) .mapTo(hashSetOf()) { (c, id) -> Address.CommunityBlindedId( serverUrl = c.baseUrl, diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index ee95c516fe..bf891abf98 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -132,7 +132,7 @@ class ReceivedMessageHandler @Inject constructor( } is DataExtractionNotification -> handleDataExtractionNotification(message) is UnsendRequest -> handleUnsendRequest(message) - is MessageRequestResponse -> messageRequestResponseHandler.get().handleExplicitRequestResponseMessage(message) + is MessageRequestResponse -> messageRequestResponseHandler.get().handleExplicitRequestResponseMessage(null, message) is VisibleMessage -> handleVisibleMessage( message = message, proto = proto, @@ -301,7 +301,7 @@ class ReceivedMessageHandler @Inject constructor( // Do nothing if the message was outdated if (messageIsOutdated(message, context.threadId)) { return null } - messageRequestResponseHandler.get().handleVisibleMessage(message) + messageRequestResponseHandler.get().handleVisibleMessage(null, message) // Handle group invite response if new closed group val threadRecipientAddress = context.threadAddress diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt index 5857cd779c..2d9ab7a35f 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt @@ -7,7 +7,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.libsession_util.ConfigBase +import network.loki.messenger.libsession_util.util.BaseCommunityInfo import network.loki.messenger.libsession_util.util.BlindKeyAPI +import network.loki.messenger.libsession_util.util.KeyPair import okio.withLock import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.userAuth @@ -36,7 +38,9 @@ import org.session.libsession.utilities.recipients.MessageType import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.getType import org.session.libsignal.protos.SignalServiceProtos +import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.BlindMappingRepository import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase @@ -53,7 +57,6 @@ import javax.inject.Singleton class ReceivedMessageProcessor @Inject constructor( @param:ApplicationContext private val context: Context, private val recipientRepository: RecipientRepository, - private val messageParser: MessageParser, private val storage: Storage, private val configFactory: ConfigFactoryProtocol, private val threadDatabase: ThreadDatabase, @@ -67,6 +70,7 @@ class ReceivedMessageProcessor @Inject constructor( private val notificationManager: MessageNotifier, private val messageRequestResponseHandler: Provider, private val visibleMessageHandler: Provider, + private val blindMappingRepository: BlindMappingRepository, ) { private val threadMutexes = ConcurrentHashMap() @@ -151,7 +155,7 @@ class ReceivedMessageProcessor @Inject constructor( } is DataExtractionNotification -> handleDataExtractionNotification(message) is UnsendRequest -> handleUnsendRequest(message) - is MessageRequestResponse -> messageRequestResponseHandler.get().handleExplicitRequestResponseMessage(message) + is MessageRequestResponse -> messageRequestResponseHandler.get().handleExplicitRequestResponseMessage(context, message) is VisibleMessage -> { if (message.isSenderSelf && message.sentTimestamp != null && @@ -160,10 +164,10 @@ class ReceivedMessageProcessor @Inject constructor( } visibleMessageHandler.get().handleVisibleMessage( + ctx = context, message = message, threadId = threadId, threadAddress = threadAddress, - ctx = context, proto = proto, runThreadUpdate = false, runProfileUpdate = true, @@ -324,19 +328,38 @@ class ReceivedMessageProcessor @Inject constructor( messageTimestamp < ctx.contactConfigTimestamp } - inner class MessageProcessingContext( - val recipients: HashMap = hashMapOf(), - val threadIDs: HashMap = hashMapOf(), - val currentUserBlindedKeys: HashMap> = hashMapOf(), - val currentUserPublicKey: String = requireNotNull(storage.getUserPublicKey()) { + inner class MessageProcessingContext { + val recipients: HashMap = hashMapOf() + val threadIDs: HashMap = hashMapOf() + val currentUserBlindedKeys: HashMap> = hashMapOf() + val currentUserId: AccountId = AccountId(requireNotNull(storage.getUserPublicKey()) { "No current user available" - }, - var maxOutgoingMessageTimestamp: Long = 0L, - ) { + }) + + var maxOutgoingMessageTimestamp: Long = 0L + + val currentUserEd25519KeyPair: KeyPair by lazy(LazyThreadSafetyMode.NONE) { + requireNotNull(storage.getUserED25519KeyPair()) { + "No current user ED25519 key pair available" + } + } + + val currentUserPublicKey: String get() = currentUserId.hexString + + val contactConfigTimestamp: Long by lazy(LazyThreadSafetyMode.NONE) { configFactory.getConfigTimestamp(UserConfigType.CONTACTS, currentUserPublicKey) } + private val blindIDMappingCache: HashMap>> = hashMapOf() + + fun getBlindIDMapping(address: Address.Standard): List> { + return blindIDMappingCache.getOrPut(address) { + blindMappingRepository.calculateReverseMappings(address) + } + } + + fun getThreadRecipient(threadAddress: Address.Conversable): Recipient { return recipients.getOrPut(threadAddress) { recipientRepository.getRecipientSync(threadAddress) @@ -345,13 +368,16 @@ class ReceivedMessageProcessor @Inject constructor( fun getCurrentUserBlindedKeysByThread(address: Address.Conversable): List { if (address !is Address.Community) return emptyList() + val serverPubKey = requireNotNull(storage.getOpenGroupPublicKey(address.serverUrl)) { + "No open group public key for community ${address.debugString}" + } return currentUserBlindedKeys.getOrPut(address) { BlindKeyAPI.blind15Ids( sessionId = currentUserPublicKey, - serverPubKey = requireNotNull(storage.getOpenGroupPublicKey(address.serverUrl)) { - "No open group public key for community ${address.debugString}" - } - ) + serverPubKey = serverPubKey + ) + BlindKeyAPI.blind25Id( + sessionId = currentUserPublicKey, + serverPubKey = serverPubKey ) } } } diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt index ae6516700d..a995e9a06f 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt @@ -49,17 +49,17 @@ class VisibleMessageHandler @Inject constructor( private val typingIndicators: SSKEnvironment.TypingIndicatorsProtocol, ){ fun handleVisibleMessage( + ctx: ReceivedMessageProcessor.MessageProcessingContext, message: VisibleMessage, threadId: Long, threadAddress: Address.Conversable, - ctx: ReceivedMessageProcessor.MessageProcessingContext, proto: SignalServiceProtos.Content, runThreadUpdate: Boolean, runProfileUpdate: Boolean, ): MessageId? { val senderAddress = message.sender!!.toAddress() - messageRequestResponseHandler.handleVisibleMessage(message) + messageRequestResponseHandler.handleVisibleMessage(ctx, message) // Handle group invite response if new closed group if (threadAddress is Address.Group && senderAddress is Address.Standard) { diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index bd00e2e2e7..859fcd75d8 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager import org.thoughtcrime.securesms.util.NetworkConnectivity +import java.util.concurrent.atomic.AtomicBoolean import kotlin.time.Duration.Companion.days private const val TAG = "Poller" @@ -215,6 +216,8 @@ class Poller @AssistedInject constructor( } } + private val profiled = AtomicBoolean(true) + private fun processPersonalMessages(messages: List) { if (messages.isEmpty()) { Log.d(TAG, "No personal messages to process") @@ -225,6 +228,13 @@ class Poller @AssistedInject constructor( val start = System.currentTimeMillis() + val shouldProfile = !profiled.getAndSet(true) + + if (shouldProfile) { + Log.d(TAG, "Start method tracing on ${Thread.currentThread().name}") + android.os.Debug.startMethodTracingSampling("${System.currentTimeMillis()}", 10 * 1024 * 1024, 1) + } + processor.startProcessing { ctx -> for (message in messages) { if (receivedMessageHashDatabase.checkOrUpdateDuplicateState( @@ -239,7 +249,9 @@ class Poller @AssistedInject constructor( try { val (message, proto) = messageParser.parse1o1Message( data = message.data, - serverHash = message.hash + serverHash = message.hash, + currentUserEd25519PrivKey = ctx.currentUserEd25519KeyPair.secretKey.data, + currentUserId = ctx.currentUserId ) processor.processEnvelopedMessage( @@ -258,6 +270,10 @@ class Poller @AssistedInject constructor( } } + if (shouldProfile) { + android.os.Debug.stopMethodTracing() + } + Log.d(TAG, "Processed ${messages.size} personal messages in ${System.currentTimeMillis() - start} ms") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index 40da70daa3..37463b809f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -483,6 +483,8 @@ class GroupPoller @AssistedInject constructor( data = message.data, serverHash = message.hash, groupId = groupId, + currentUserId = ctx.currentUserId, + currentUserEd25519PrivKey = ctx.currentUserEd25519KeyPair.secretKey.data, ) receivedMessageProcessor.processEnvelopedMessage( diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt index a78074137e..7b78990f71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -102,13 +102,15 @@ class PushReceiver @Inject constructor( namespace = namespace, hash = pushData.metadata.msg_hash )) { - val (msg, proto) = messageParser.parseGroupMessage( - data = pushData.data, - serverHash = pushData.metadata.msg_hash, - groupId = groupId - ) - receivedMessageProcessor.startProcessing { ctx -> + val (msg, proto) = messageParser.parseGroupMessage( + data = pushData.data, + serverHash = pushData.metadata.msg_hash, + groupId = groupId, + currentUserId = ctx.currentUserId, + currentUserEd25519PrivKey = ctx.currentUserEd25519KeyPair.secretKey.data + ) + receivedMessageProcessor.processEnvelopedMessage( threadAddress = Address.Group(groupId), message = msg, @@ -168,12 +170,14 @@ class PushReceiver @Inject constructor( namespace = Namespace.DEFAULT(), hash = pushData.metadata.msg_hash )) { - val (message, proto) = messageParser.parse1o1Message( - data = pushData.data, - serverHash = pushData.metadata.msg_hash, - ) - receivedMessageProcessor.startProcessing { ctx -> + val (message, proto) = messageParser.parse1o1Message( + data = pushData.data, + serverHash = pushData.metadata.msg_hash, + currentUserId = ctx.currentUserId, + currentUserEd25519PrivKey = ctx.currentUserEd25519KeyPair.secretKey.data, + ) + receivedMessageProcessor.processEnvelopedMessage( threadAddress = message.senderOrSync.toAddress() as Address.Conversable, message = message, From 82b89c53e3c235eaeadb0d40b6d04a3d1225b1ab Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:39:15 +1100 Subject: [PATCH 15/19] Community message handling --- .../sending_receiving/MessageParser.kt | 43 ++-- .../ReceivedMessageProcessor.kt | 242 ++++++++++++++---- .../VisibleMessageHandler.kt | 10 +- .../pollers/OpenGroupPoller.kt | 171 +++++-------- .../sending_receiving/pollers/Poller.kt | 3 +- .../securesms/groups/GroupPoller.kt | 2 +- .../securesms/notifications/PushReceiver.kt | 4 +- 7 files changed, 296 insertions(+), 179 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt index 8165e7099c..5b0085a963 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt @@ -15,6 +15,7 @@ import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.visible.ParsedMessage import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupMessage import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.ConfigFactoryProtocol @@ -69,7 +70,7 @@ class MessageParser @Inject constructor( checkForBlockStatus: Boolean, isForGroup: Boolean, currentUserId: AccountId, - alternativeCurrentUserIds: List, + currentUserBlindedIDs: List, senderIdPrefix: IdPrefix ): Pair { return parseMessage( @@ -80,7 +81,7 @@ class MessageParser @Inject constructor( checkForBlockStatus = checkForBlockStatus, isForGroup = isForGroup, currentUserId = currentUserId, - alternativeCurrentUserIds = alternativeCurrentUserIds, + currentUserBlindedIDs = currentUserBlindedIDs, ) } @@ -92,7 +93,7 @@ class MessageParser @Inject constructor( checkForBlockStatus: Boolean, isForGroup: Boolean, currentUserId: AccountId, - alternativeCurrentUserIds: List, + currentUserBlindedIDs: List, ): Pair { val proto = SignalServiceProtos.Content.parseFrom(contentPlaintext) @@ -114,7 +115,7 @@ class MessageParser @Inject constructor( } // Valid self-send messages - val isSenderSelf = sender == currentUserId || sender in alternativeCurrentUserIds + val isSenderSelf = sender == currentUserId || sender in currentUserBlindedIDs if (isSenderSelf && !message.isSelfSendValid) { throw NonRetryableException("Ignoring self send message") } @@ -165,7 +166,7 @@ class MessageParser @Inject constructor( isForGroup = false, senderIdPrefix = IdPrefix.STANDARD, currentUserId = currentUserId, - alternativeCurrentUserIds = emptyList(), + currentUserBlindedIDs = emptyList(), ).also { (message, _) -> message.serverHash = serverHash } @@ -198,29 +199,28 @@ class MessageParser @Inject constructor( isForGroup = true, senderIdPrefix = IdPrefix.STANDARD, currentUserId = currentUserId, - alternativeCurrentUserIds = emptyList(), + currentUserBlindedIDs = emptyList(), ).also { (message, _) -> message.serverHash = serverHash } } fun parseCommunityMessage( - msg: OpenGroupMessage, - communityServerPubKeyHex: String, - ): Pair { + msg: OpenGroupApi.Message, + currentUserId: AccountId, + currentUserBlindedIDs: List, + ): Pair? { + if (msg.data.isNullOrBlank()) { + return null + } + val decoded = SessionProtocol.decodeForCommunity( - payload = Base64.decode(msg.base64EncodedData.orEmpty()), + payload = Base64.decode(msg.data), nowEpochMs = snodeClock.currentTimeMills(), proBackendPubKey = proBackendKey, ) - val sender = AccountId(msg.sender!!) - - val keyPair = requireNotNull(storage.getUserED25519KeyPair()) { - "Couldn't find current user's key" - } - - val currentUserId = AccountId(IdPrefix.STANDARD, keyPair.pubKey.data) + val sender = AccountId(msg.sessionId) return parseMessage( contentPlaintext = decoded.contentPlainText.data, @@ -229,13 +229,10 @@ class MessageParser @Inject constructor( isForGroup = false, currentUserId = currentUserId, sender = sender, - messageTimestampMs = msg.sentTimestamp, - alternativeCurrentUserIds = BlindKeyAPI.blind15Ids( - sessionId = currentUserId.hexString, - serverPubKey = communityServerPubKeyHex, - ).map(::AccountId), + messageTimestampMs = (msg.posted * 1000).toLong(), + currentUserBlindedIDs = currentUserBlindedIDs, ).also { (message, _) -> - message.openGroupServerMessageID = msg.serverID + message.openGroupServerMessageID = msg.id } } diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt index 2d9ab7a35f..32c92756ef 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt @@ -1,6 +1,8 @@ package org.session.libsession.messaging.sending_receiving import android.content.Context +import androidx.compose.runtime.saveable.autoSaver +import androidx.compose.ui.geometry.Rect import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -23,7 +25,7 @@ import org.session.libsession.messaging.messages.control.ReadReceipt import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.visible.VisibleMessage -import org.session.libsession.messaging.open_groups.OpenGroupMessage +import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier import org.session.libsession.messaging.utilities.WebRtcUtils @@ -45,6 +47,7 @@ import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager import java.util.concurrent.ConcurrentHashMap @@ -71,6 +74,7 @@ class ReceivedMessageProcessor @Inject constructor( private val messageRequestResponseHandler: Provider, private val visibleMessageHandler: Provider, private val blindMappingRepository: BlindMappingRepository, + private val messageParser: MessageParser, ) { private val threadMutexes = ConcurrentHashMap() @@ -81,20 +85,34 @@ class ReceivedMessageProcessor @Inject constructor( * * Note: the context passed to the block is not thread-safe, so it should not be shared between threads. */ - fun startProcessing(block: (MessageProcessingContext) -> T): T { + fun startProcessing(debugName: String, block: (MessageProcessingContext) -> T): T { val context = MessageProcessingContext() + val start = System.currentTimeMillis() try { return block(context) } finally { for (threadId in context.threadIDs.values) { if (context.maxOutgoingMessageTimestamp > 0L && - context.maxOutgoingMessageTimestamp > storage.getLastSeen(threadId)) { - storage.markConversationAsRead(threadId, context.maxOutgoingMessageTimestamp, force = true) + context.maxOutgoingMessageTimestamp > storage.getLastSeen(threadId) + ) { + storage.markConversationAsRead( + threadId, + context.maxOutgoingMessageTimestamp, + force = true + ) } storage.updateThread(threadId, true) notificationManager.updateNotification(this.context, threadId) } + + // Handle pending community reactions + context.pendingCommunityReactions?.let { reactions -> + storage.addReactions(reactions, replaceAll = true, notifyUnread = false) + reactions.clear() + } + + Log.d(TAG, "Processed messages for $debugName in ${System.currentTimeMillis() - start}ms") } } @@ -118,21 +136,20 @@ class ReceivedMessageProcessor @Inject constructor( } // Get or create thread ID, if we aren't allowed to create it, and it doesn't exist, drop the message - val threadId = context.threadIDs[threadAddress] ?: - if (shouldCreateThread(message)) { - threadDatabase.getOrCreateThreadIdFor(threadAddress) - .also { context.threadIDs[threadAddress] = it } - } else { - threadDatabase.getThreadIdIfExistsFor(threadAddress) - .also { id -> - if (id == -1L) { - log { "Dropping message for non-existing thread ${threadAddress.debugString}" } - return@withLock - } else { - context.threadIDs[threadAddress] = id - } + val threadId = context.threadIDs[threadAddress] ?: if (shouldCreateThread(message)) { + threadDatabase.getOrCreateThreadIdFor(threadAddress) + .also { context.threadIDs[threadAddress] = it } + } else { + threadDatabase.getThreadIdIfExistsFor(threadAddress) + .also { id -> + if (id == -1L) { + log { "Dropping message for non-existing thread ${threadAddress.debugString}" } + return@withLock + } else { + context.threadIDs[threadAddress] = id } - } + } + } when (message) { is ReadReceipt -> handleReadReceipt(message) @@ -141,6 +158,7 @@ class ReceivedMessageProcessor @Inject constructor( message = message, groupId = (threadAddress as? Address.Group)?.accountId ) + is ExpirationTimerUpdate -> { // For groupsv2, there are dedicated mechanisms for handling expiration timers, and // we want to avoid the 1-to-1 message format which is unauthenticated in a group settings. @@ -153,13 +171,17 @@ class ReceivedMessageProcessor @Inject constructor( handleExpirationTimerUpdate(message) } } + is DataExtractionNotification -> handleDataExtractionNotification(message) is UnsendRequest -> handleUnsendRequest(message) - is MessageRequestResponse -> messageRequestResponseHandler.get().handleExplicitRequestResponseMessage(context, message) + is MessageRequestResponse -> messageRequestResponseHandler.get() + .handleExplicitRequestResponseMessage(context, message) + is VisibleMessage -> { if (message.isSenderSelf && message.sentTimestamp != null && - message.sentTimestamp!! > context.maxOutgoingMessageTimestamp) { + message.sentTimestamp!! > context.maxOutgoingMessageTimestamp + ) { context.maxOutgoingMessageTimestamp = message.sentTimestamp!! } @@ -180,11 +202,86 @@ class ReceivedMessageProcessor @Inject constructor( } fun processCommunityMessage( + context: MessageProcessingContext, threadAddress: Address.Community, - message: OpenGroupMessage, - context: MessageProcessingContext + message: OpenGroupApi.Message, ) = threadMutexes.getOrPut(threadAddress) { ReentrantLock() }.withLock { + var messageId = messageParser.parseCommunityMessage( + msg = message, + currentUserId = context.currentUserId, + currentUserBlindedIDs = context.getCurrentUserBlindedIDsByThread(threadAddress) + )?.let { (msg, proto) -> + processEnvelopedMessage( + context = context, + threadAddress = threadAddress, + message = msg, + proto = proto + ) + msg.id + } + + // For community, we have a different way of handling reaction, this is outside of + // the normal enveloped message (even though enveloped message can also contain reaction, + // it's not used by anyone at the moment). + if (messageId == null) { + Log.d(TAG, "Handling reactions only message for community ${threadAddress.debugString}") + messageId = requireNotNull( + messageDataProvider.getMessageID( + serverId = message.id, + threadId = requireNotNull(storage.getThreadId(threadAddress)) { + "No thread ID for community ${threadAddress.debugString}" + } + )) { + "No message persisted for community message ${message.id}" + } + } + + val messageServerId = message.id.toString() + + for ((emoji, reaction) in message.reactions.orEmpty()) { + // We only really want up to 5 reactors per reaction to avoid excessive database load + // Among the 5 reactors, we must include ourselves if we reacted to this message + val otherReactorsToAdd = if (reaction.you) { + context.addPendingCommunityReaction( + messageId, + ReactionRecord( + messageId = messageId, + author = context.currentUserPublicKey, + emoji = emoji, + serverId = messageServerId, + count = reaction.count, + sortId = 0, + ) + ) + + val myBlindedIDs = context.getCurrentUserBlindedIDsByThread(threadAddress) + + reaction.reactors + .asSequence() + .filterNot { reactor -> reactor == context.currentUserPublicKey || myBlindedIDs.any { it.hexString == reactor } } + .take(4) + } else { + reaction.reactors + .asSequence() + .take(5) + } + + + for (reactor in otherReactorsToAdd) { + context.addPendingCommunityReaction( + messageId, + ReactionRecord( + messageId = messageId, + author = reactor, + emoji = emoji, + serverId = messageServerId, + count = reaction.count, + sortId = reaction.index, + ) + ) + } + } } private fun handleReadReceipt(message: ReadReceipt) { @@ -204,7 +301,7 @@ class ReceivedMessageProcessor @Inject constructor( private fun showTypingIndicatorIfNeeded(senderPublicKey: String) { // We don't want to show other people's indicators if the toggle is off - if(!prefs.isTypingIndicatorsEnabled()) return + if (!prefs.isTypingIndicatorsEnabled()) return val address = Address.fromSerialized(senderPublicKey) val threadID = storage.getThreadId(address) ?: return @@ -237,11 +334,18 @@ class ReceivedMessageProcessor @Inject constructor( if (message.groupPublicKey != null) return val senderPublicKey = message.sender!! - val notification: DataExtractionNotificationInfoMessage = when(message.kind) { - is DataExtractionNotification.Kind.MediaSaved -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) + val notification: DataExtractionNotificationInfoMessage = when (message.kind) { + is DataExtractionNotification.Kind.MediaSaved -> DataExtractionNotificationInfoMessage( + DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED + ) + else -> return } - storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!) + storage.insertDataExtractionNotificationMessage( + senderPublicKey, + notification, + message.sentTimestamp!! + ) } fun handleUnsendRequest(message: UnsendRequest): MessageId? { @@ -251,7 +355,7 @@ class ReceivedMessageProcessor @Inject constructor( var admin = false val groupID = doubleEncodeGroupID(key) val group = storage.getGroup(groupID) - if(group != null) { + if (group != null) { admin = group.admins.map { it.toString() }.contains(message.sender) } admin @@ -259,11 +363,14 @@ class ReceivedMessageProcessor @Inject constructor( // First we need to determine the validity of the UnsendRequest // It is valid if: - val requestIsValid = message.sender == message.author || // the sender is the author of the message - message.author == userPublicKey || // the sender is the current user - isLegacyGroupAdmin // sender is an admin of legacy group + val requestIsValid = + message.sender == message.author || // the sender is the author of the message + message.author == userPublicKey || // the sender is the current user + isLegacyGroupAdmin // sender is an admin of legacy group - if (!requestIsValid) { return null } + if (!requestIsValid) { + return null + } val timestamp = message.timestamp ?: return null val author = message.author ?: return null @@ -286,7 +393,7 @@ class ReceivedMessageProcessor @Inject constructor( // the message is marked as deleted locally // except for 'note to self' where the message is completely deleted - if (messageType == MessageType.NOTE_TO_SELF){ + if (messageType == MessageType.NOTE_TO_SELF) { messageDataProvider.deleteMessage(messageIdToDelete) } else { messageDataProvider.markMessageAsDeleted( @@ -312,13 +419,14 @@ class ReceivedMessageProcessor @Inject constructor( } - /** * Return true if the contact is marked as hidden for given message timestamp. */ - private fun shouldDiscardForHiddenContact(ctx: MessageProcessingContext, - messageTimestamp: Long, - threadAddress: Address.Standard): Boolean { + private fun shouldDiscardForHiddenContact( + ctx: MessageProcessingContext, + messageTimestamp: Long, + threadAddress: Address.Standard + ): Boolean { val hidden = configFactory.withUserConfigs { configs -> configs.contacts.get(threadAddress.address)?.priority == ConfigBase.PRIORITY_HIDDEN } @@ -328,10 +436,17 @@ class ReceivedMessageProcessor @Inject constructor( messageTimestamp < ctx.contactConfigTimestamp } + /** + * A context object for processing received messages. This object is mostly used to store + * expensive data that are only valid for the duration of a processing session. + * + * It also tracks some deferred updates that should be applied once processing is complete, + * such as thread updates, reactions, and notifications. + */ inner class MessageProcessingContext { - val recipients: HashMap = hashMapOf() + private var recipients: HashMap? = null val threadIDs: HashMap = hashMapOf() - val currentUserBlindedKeys: HashMap> = hashMapOf() + private var currentUserBlindedKeys: HashMap>? = null val currentUserId: AccountId = AccountId(requireNotNull(storage.getUserPublicKey()) { "No current user available" }) @@ -351,35 +466,70 @@ class ReceivedMessageProcessor @Inject constructor( configFactory.getConfigTimestamp(UserConfigType.CONTACTS, currentUserPublicKey) } - private val blindIDMappingCache: HashMap>> = hashMapOf() + private var blindIDMappingCache: HashMap>>? = + null + + + var pendingCommunityReactions: HashMap>? = null + private set + fun getBlindIDMapping(address: Address.Standard): List> { - return blindIDMappingCache.getOrPut(address) { + val cache = blindIDMappingCache + ?: hashMapOf>>().also { + blindIDMappingCache = it + } + + return cache.getOrPut(address) { blindMappingRepository.calculateReverseMappings(address) } } fun getThreadRecipient(threadAddress: Address.Conversable): Recipient { - return recipients.getOrPut(threadAddress) { + val cache = recipients ?: hashMapOf().also { + recipients = it + } + + return cache.getOrPut(threadAddress) { recipientRepository.getRecipientSync(threadAddress) } } - fun getCurrentUserBlindedKeysByThread(address: Address.Conversable): List { + fun getCurrentUserBlindedIDsByThread(address: Address.Conversable): List { if (address !is Address.Community) return emptyList() val serverPubKey = requireNotNull(storage.getOpenGroupPublicKey(address.serverUrl)) { "No open group public key for community ${address.debugString}" } - return currentUserBlindedKeys.getOrPut(address) { + + val cache = + currentUserBlindedKeys ?: hashMapOf>().also { + currentUserBlindedKeys = it + } + + return cache.getOrPut(address) { BlindKeyAPI.blind15Ids( sessionId = currentUserPublicKey, serverPubKey = serverPubKey - ) + BlindKeyAPI.blind25Id( - sessionId = currentUserPublicKey, - serverPubKey = serverPubKey ) + ).map(::AccountId) + AccountId( + BlindKeyAPI.blind25Id( + sessionId = currentUserPublicKey, + serverPubKey = serverPubKey + ) + ) } } + + fun addPendingCommunityReaction(messageId: MessageId, reaction: ReactionRecord) { + val reactionsMap = pendingCommunityReactions + ?: hashMapOf>().also { + pendingCommunityReactions = it + } + + reactionsMap.getOrPut(messageId) { + mutableListOf() + }.add(reaction) + } } companion object { diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt index a995e9a06f..06f9213bf0 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt @@ -82,10 +82,10 @@ class VisibleMessageHandler @Inject constructor( if (message.quote != null && proto.dataMessage.hasQuote()) { val quote = proto.dataMessage.quote - val author = if (quote.author in ctx.getCurrentUserBlindedKeysByThread(threadAddress)) { - Address.fromSerialized(ctx.currentUserPublicKey) - } else { - Address.fromSerialized(quote.author) + var author = quote.author.toAddress() + + if (author is Address.WithAccountId && author.accountId in ctx.getCurrentUserBlindedIDsByThread(threadAddress)) { + author = Address.Standard(ctx.currentUserId) } val messageInfo = messageDataProvider.getMessageForQuote(threadId, quote.id, author) @@ -149,7 +149,7 @@ class VisibleMessageHandler @Inject constructor( val maxChars = proStatusManager.getIncomingMessageMaxLength(message) val messageText = message.text?.take(maxChars) // truncate to max char limit for this message message.text = messageText - message.hasMention = (sequenceOf(ctx.currentUserPublicKey) + ctx.getCurrentUserBlindedKeysByThread(threadAddress).asSequence()) + message.hasMention = (sequenceOf(ctx.currentUserPublicKey) + ctx.getCurrentUserBlindedIDsByThread(threadAddress).asSequence()) .any { key -> messageText?.contains("@$key") == true || key == (quoteModel?.author?.toString() ?: "") } diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 82349bdd4c..f959cc2d71 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -1,7 +1,6 @@ package org.session.libsession.messaging.sending_receiving.pollers import com.fasterxml.jackson.core.type.TypeReference -import com.google.protobuf.ByteString import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -18,14 +17,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import org.session.libsession.database.StorageProtocol -import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.MessageReceiveParameters import org.session.libsession.messaging.jobs.OpenGroupDeleteJob import org.session.libsession.messaging.jobs.TrimThreadJob -import org.session.libsession.messaging.messages.Message.Companion.senderOrSync -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate -import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.Endpoint import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupApi.BatchRequest @@ -36,15 +30,9 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi.DirectMessage import org.session.libsession.messaging.open_groups.OpenGroupApi.Message import org.session.libsession.messaging.open_groups.OpenGroupApi.getOrFetchServerCapabilities import org.session.libsession.messaging.open_groups.OpenGroupApi.parallelBatch -import org.session.libsession.messaging.open_groups.OpenGroupMessage -import org.session.libsession.messaging.sending_receiving.MessageReceiver -import org.session.libsession.messaging.sending_receiving.ReceivedMessageHandler +import org.session.libsession.messaging.sending_receiving.ReceivedMessageProcessor import org.session.libsession.utilities.Address -import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsignal.protos.SignalServiceProtos -import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.HTTP.Verb.GET import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log @@ -52,7 +40,6 @@ import org.thoughtcrime.securesms.database.BlindMappingRepository import org.thoughtcrime.securesms.database.CommunityDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager -import java.util.concurrent.TimeUnit private typealias PollRequestToken = Channel>> @@ -68,14 +55,12 @@ class OpenGroupPoller @AssistedInject constructor( private val storage: StorageProtocol, private val appVisibilityManager: AppVisibilityManager, private val blindMappingRepository: BlindMappingRepository, - private val receivedMessageHandler: ReceivedMessageHandler, - private val batchMessageJobFactory: BatchMessageReceiveJob.Factory, private val configFactory: ConfigFactoryProtocol, private val threadDatabase: ThreadDatabase, private val trimThreadJobFactory: TrimThreadJob.Factory, private val openGroupDeleteJobFactory: OpenGroupDeleteJob.Factory, private val communityDatabase: CommunityDatabase, - private val messageReceiver: MessageReceiver, + private val receivedMessageProcessor: ReceivedMessageProcessor, @Assisted private val server: String, @Assisted private val scope: CoroutineScope, @Assisted private val pollerSemaphore: Semaphore, @@ -300,20 +285,8 @@ class OpenGroupPoller @AssistedInject constructor( messages: List ) { val sortedMessages = messages.sortedBy { it.seqno } - sortedMessages.maxOfOrNull { it.seqno }?.let { seqNo -> - storage.setLastMessageServerID(roomToken, server, seqNo) - } - val (deletions, additions) = sortedMessages.partition { it.deleted } - handleNewMessages(server, roomToken, additions.map { - OpenGroupMessage( - serverID = it.id, - sender = it.sessionId, - sentTimestamp = (it.posted * 1000).toLong(), - base64EncodedData = it.data, - base64EncodedSignature = it.signature, - reactions = it.reactions - ) - }) + val (deletions, additions) = messages.partition { it.deleted } + handleNewMessages(server, roomToken, additions) handleDeletedMessages(server, roomToken, deletions.map { it.id }) } @@ -331,85 +304,83 @@ class OpenGroupPoller @AssistedInject constructor( } else { storage.setLastInboxMessageId(server, lastMessageId) } - sortedMessages.forEach { - val encodedMessage = Base64.decode(it.message) - val envelope = SignalServiceProtos.Envelope.newBuilder() - .setTimestampMs(TimeUnit.SECONDS.toMillis(it.postedAt)) - .setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE) - .setContent(ByteString.copyFrom(encodedMessage)) - .setSource(it.sender) - .build() - try { - val (message, proto) = messageReceiver.parse( - envelope.toByteArray(), - null, - fromOutbox, - if (fromOutbox) it.recipient else it.sender, - serverPublicKey, - emptySet() // this shouldn't be necessary as we are polling open groups here - ) - if (fromOutbox) { - val syncTarget = blindMappingRepository.getMapping( - serverUrl = server, - blindedAddress = Address.Blinded(AccountId(it.recipient)) - )?.accountId?.hexString ?: it.recipient - - if (message is VisibleMessage) { - message.syncTarget = syncTarget - } else if (message is ExpirationTimerUpdate) { - message.syncTarget = syncTarget - } - } - val threadAddress = when (val addr = message.senderOrSync.toAddress()) { - is Address.Blinded -> Address.CommunityBlindedId(serverUrl = server, blindedId = addr) - is Address.Conversable -> addr - else -> throw IllegalArgumentException("Unsupported address type: ${addr.debugString}") - } - val threadId = threadDatabase.getThreadIdIfExistsFor(threadAddress) - receivedMessageHandler.handle( - message = message, - proto = proto, - threadId = threadId, - threadAddress = threadAddress, - ) - } catch (e: Exception) { - Log.e(TAG, "Couldn't handle direct message", e) - } + sortedMessages.forEach { + //TODO: implement direct message handling + +// val encodedMessage = Base64.decode(it.message) +// val envelope = SignalServiceProtos.Envelope.newBuilder() +// .setTimestampMs(TimeUnit.SECONDS.toMillis(it.postedAt)) +// .setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE) +// .setContent(ByteString.copyFrom(encodedMessage)) +// .setSource(it.sender) +// .build() +// try { +// val (message, proto) = messageReceiver.parse( +// envelope.toByteArray(), +// null, +// fromOutbox, +// if (fromOutbox) it.recipient else it.sender, +// serverPublicKey, +// emptySet() // this shouldn't be necessary as we are polling open groups here +// ) +// if (fromOutbox) { +// val syncTarget = blindMappingRepository.getMapping( +// serverUrl = server, +// blindedAddress = Address.Blinded(AccountId(it.recipient)) +// )?.accountId?.hexString ?: it.recipient +// +// if (message is VisibleMessage) { +// message.syncTarget = syncTarget +// } else if (message is ExpirationTimerUpdate) { +// message.syncTarget = syncTarget +// } +// } +// val threadAddress = when (val addr = message.senderOrSync.toAddress()) { +// is Address.Blinded -> Address.CommunityBlindedId(serverUrl = server, blindedId = addr) +// is Address.Conversable -> addr +// else -> throw IllegalArgumentException("Unsupported address type: ${addr.debugString}") +// } +// +// val threadId = threadDatabase.getThreadIdIfExistsFor(threadAddress) +// receivedMessageHandler.handle( +// message = message, +// proto = proto, +// threadId = threadId, +// threadAddress = threadAddress, +// ) +// } catch (e: Exception) { +// Log.e(TAG, "Couldn't handle direct message", e) +// } } } - private fun handleNewMessages(server: String, roomToken: String, messages: List) { + private fun handleNewMessages(server: String, roomToken: String, sortedMessages: List) { val threadAddress = Address.Community(serverUrl = server, room = roomToken) // check thread still exists val threadId = storage.getThreadId(threadAddress) ?: return - val envelopes = mutableListOf?>>() - messages.sortedBy { it.serverID!! }.forEach { message -> - if (!message.base64EncodedData.isNullOrEmpty()) { - val envelope = SignalServiceProtos.Envelope.newBuilder() - .setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE) - .setSource(message.sender!!) - .setSourceDevice(1) - .setContent(message.toProto().toByteString()) - .setTimestampMs(message.sentTimestamp) - .build() - envelopes.add(Triple( message.serverID, envelope, message.reactions)) - } - } - //TODO: Re-enable community polling + if (sortedMessages.isEmpty()) return -// envelopes.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { list -> -// val parameters = list.map { (serverId, message, reactions) -> -// MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId, reactions = reactions) -// } -// JobQueue.shared.add(batchMessageJobFactory.create( -// parameters, -// fromCommunity = threadAddress -// )) -// } + receivedMessageProcessor.startProcessing("CommunityPoller(${threadAddress.debugString})") { ctx -> + for (msg in sortedMessages) { + try { + // Set the last message server ID to each message as we process them, so that if processing fails halfway through, + // we don't re-process messages we've already handled. + storage.setLastMessageServerID(roomToken, server, msg.id) + + receivedMessageProcessor.processCommunityMessage( + context = ctx, + threadAddress = threadAddress, + message = msg, + ) + } catch (e: Exception) { + Log.e(TAG, "Error processing open group message ${msg.id} in ${threadAddress.debugString}", e) + } + } + } - if (envelopes.isNotEmpty()) { + if (sortedMessages.isNotEmpty()) { JobQueue.shared.add(trimThreadJobFactory.create(threadId)) } } diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 859fcd75d8..1db9ee34ae 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -65,7 +65,6 @@ class Poller @AssistedInject constructor( private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, private val processor: ReceivedMessageProcessor, private val messageParser: MessageParser, - private val threadDatabase: ThreadDatabase, @Assisted scope: CoroutineScope ) { private val userPublicKey: String @@ -235,7 +234,7 @@ class Poller @AssistedInject constructor( android.os.Debug.startMethodTracingSampling("${System.currentTimeMillis()}", 10 * 1024 * 1024, 1) } - processor.startProcessing { ctx -> + processor.startProcessing("Poller") { ctx -> for (message in messages) { if (receivedMessageHashDatabase.checkOrUpdateDuplicateState( swarmPublicKey = userPublicKey, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index 37463b809f..00377a978e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -467,7 +467,7 @@ class GroupPoller @AssistedInject constructor( val start = System.currentTimeMillis() val threadAddress = Address.Group(groupId) - receivedMessageProcessor.startProcessing { ctx -> + receivedMessageProcessor.startProcessing("GroupPoller($groupId)") { ctx -> for (message in messages) { if (receivedMessageHashDatabase.checkOrUpdateDuplicateState( swarmPublicKey = groupId.hexString, diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt index 7b78990f71..c80251b105 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -102,7 +102,7 @@ class PushReceiver @Inject constructor( namespace = namespace, hash = pushData.metadata.msg_hash )) { - receivedMessageProcessor.startProcessing { ctx -> + receivedMessageProcessor.startProcessing("GroupPushReceive($groupId)") { ctx -> val (msg, proto) = messageParser.parseGroupMessage( data = pushData.data, serverHash = pushData.metadata.msg_hash, @@ -170,7 +170,7 @@ class PushReceiver @Inject constructor( namespace = Namespace.DEFAULT(), hash = pushData.metadata.msg_hash )) { - receivedMessageProcessor.startProcessing { ctx -> + receivedMessageProcessor.startProcessing("PushReceiver") { ctx -> val (message, proto) = messageParser.parse1o1Message( data = pushData.data, serverHash = pushData.metadata.msg_hash, From ba39390348d018475e2bace6e601e95415946b38 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:43:04 +1100 Subject: [PATCH 16/19] Updated community processor --- .../sending_receiving/MessageParser.kt | 14 +- .../ReceivedMessageProcessor.kt | 37 +++- .../pollers/OpenGroupPoller.kt | 190 ++++++++---------- gradle/libs.versions.toml | 2 +- 4 files changed, 115 insertions(+), 128 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt index 5b0085a963..91d656d266 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt @@ -2,7 +2,6 @@ package org.session.libsession.messaging.sending_receiving import network.loki.messenger.libsession_util.protocol.DecodedEnvelope import network.loki.messenger.libsession_util.protocol.SessionProtocol -import network.loki.messenger.libsession_util.util.BlindKeyAPI import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.CallMessage @@ -13,10 +12,8 @@ import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.ReadReceipt import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.UnsendRequest -import org.session.libsession.messaging.messages.visible.ParsedMessage import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.messaging.open_groups.OpenGroupMessage import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsignal.exceptions.NonRetryableException @@ -148,7 +145,7 @@ class MessageParser @Inject constructor( fun parse1o1Message( data: ByteArray, - serverHash: String, + serverHash: String?, currentUserEd25519PrivKey: ByteArray, currentUserId: AccountId, ): Pair { @@ -235,13 +232,4 @@ class MessageParser @Inject constructor( message.openGroupServerMessageID = msg.id } } - - fun parseCommunityInboxMessage( - data: ByteArray, - isOutgoing: Boolean, - otherBlindedPublicKey: String, - communityServerPubKeyHex: String, - ): ParsedMessage { - TODO() - } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt index 32c92756ef..d713fce7b8 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt @@ -31,6 +31,7 @@ import org.session.libsession.messaging.sending_receiving.notifications.MessageN import org.session.libsession.messaging.utilities.WebRtcUtils import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID import org.session.libsession.utilities.SSKEnvironment @@ -41,6 +42,7 @@ import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.getType import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.BlindMappingRepository import org.thoughtcrime.securesms.database.RecipientRepository @@ -201,6 +203,20 @@ class ReceivedMessageProcessor @Inject constructor( } + fun processCommunityInboxMessage( + context: MessageProcessingContext, + message: OpenGroupApi.DirectMessage + ) { + //TODO("Waiting for the implementation from libsession_util") + } + + fun processCommunityOutboxMessage( + context: MessageProcessingContext, + message: OpenGroupApi.DirectMessage + ) { + //TODO("Waiting for the implementation from libsession_util") + } + fun processCommunityMessage( context: MessageProcessingContext, threadAddress: Address.Community, @@ -446,7 +462,7 @@ class ReceivedMessageProcessor @Inject constructor( inner class MessageProcessingContext { private var recipients: HashMap? = null val threadIDs: HashMap = hashMapOf() - private var currentUserBlindedKeys: HashMap>? = null + private var currentUserBlindedKeysByCommunityServer: HashMap>? = null val currentUserId: AccountId = AccountId(requireNotNull(storage.getUserPublicKey()) { "No current user available" }) @@ -496,18 +512,17 @@ class ReceivedMessageProcessor @Inject constructor( } } - fun getCurrentUserBlindedIDsByThread(address: Address.Conversable): List { - if (address !is Address.Community) return emptyList() - val serverPubKey = requireNotNull(storage.getOpenGroupPublicKey(address.serverUrl)) { - "No open group public key for community ${address.debugString}" + fun getCurrentUserBlindedIDsByServer(serverUrl: String): List { + val serverPubKey = requireNotNull(storage.getOpenGroupPublicKey(serverUrl)) { + "No open group public key found" } val cache = - currentUserBlindedKeys ?: hashMapOf>().also { - currentUserBlindedKeys = it + currentUserBlindedKeysByCommunityServer ?: hashMapOf>().also { + currentUserBlindedKeysByCommunityServer = it } - return cache.getOrPut(address) { + return cache.getOrPut(serverUrl) { BlindKeyAPI.blind15Ids( sessionId = currentUserPublicKey, serverPubKey = serverPubKey @@ -520,6 +535,12 @@ class ReceivedMessageProcessor @Inject constructor( } } + + fun getCurrentUserBlindedIDsByThread(address: Address.Conversable): List { + if (address !is Address.Community) return emptyList() + return getCurrentUserBlindedIDsByServer(address.serverUrl) + } + fun addPendingCommunityReaction(messageId: MessageId, reaction: ReactionRecord) { val reactionsMap = pendingCommunityReactions ?: hashMapOf>().also { diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index f959cc2d71..9ad9213197 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -20,6 +20,7 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.OpenGroupDeleteJob import org.session.libsession.messaging.jobs.TrimThreadJob +import org.session.libsession.messaging.messages.Message.Companion.senderOrSync import org.session.libsession.messaging.open_groups.Endpoint import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupApi.BatchRequest @@ -27,18 +28,18 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi.BatchRequestInf import org.session.libsession.messaging.open_groups.OpenGroupApi.BatchResponse import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability import org.session.libsession.messaging.open_groups.OpenGroupApi.DirectMessage -import org.session.libsession.messaging.open_groups.OpenGroupApi.Message import org.session.libsession.messaging.open_groups.OpenGroupApi.getOrFetchServerCapabilities import org.session.libsession.messaging.open_groups.OpenGroupApi.parallelBatch +import org.session.libsession.messaging.sending_receiving.MessageParser import org.session.libsession.messaging.sending_receiving.ReceivedMessageProcessor import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.HTTP.Verb.GET import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.database.BlindMappingRepository import org.thoughtcrime.securesms.database.CommunityDatabase -import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager private typealias PollRequestToken = Channel>> @@ -54,13 +55,12 @@ private typealias PollRequestToken = Channel>> class OpenGroupPoller @AssistedInject constructor( private val storage: StorageProtocol, private val appVisibilityManager: AppVisibilityManager, - private val blindMappingRepository: BlindMappingRepository, private val configFactory: ConfigFactoryProtocol, - private val threadDatabase: ThreadDatabase, private val trimThreadJobFactory: TrimThreadJob.Factory, private val openGroupDeleteJobFactory: OpenGroupDeleteJob.Factory, private val communityDatabase: CommunityDatabase, private val receivedMessageProcessor: ReceivedMessageProcessor, + private val messageParser: MessageParser, @Assisted private val server: String, @Assisted private val scope: CoroutineScope, @Assisted private val pollerSemaphore: Semaphore, @@ -154,8 +154,6 @@ class OpenGroupPoller @AssistedInject constructor( return emptyList() } - val publicKey = allCommunities.first { it.community.baseUrl == server }.community.pubKeyHex - poll(rooms) .asSequence() .filterNot { it.body == null } @@ -165,16 +163,16 @@ class OpenGroupPoller @AssistedInject constructor( handleRoomPollInfo(Address.Community(server, response.endpoint.roomToken), response.body as Map<*, *>) } is Endpoint.RoomMessagesRecent -> { - handleMessages(server, response.endpoint.roomToken, response.body as List) + handleMessages(response.endpoint.roomToken, response.body as List) } is Endpoint.RoomMessagesSince -> { - handleMessages(server, response.endpoint.roomToken, response.body as List) + handleMessages(response.endpoint.roomToken, response.body as List) } is Endpoint.Inbox, is Endpoint.InboxSince -> { - handleDirectMessages(server, false, response.body as List) + handleInboxMessages( response.body as List) } is Endpoint.Outbox, is Endpoint.OutboxSince -> { - handleDirectMessages(server, true, response.body as List) + handleOutboxMessages( response.body as List) } else -> { /* We don't care about the result of any other calls (won't be polled for) */} } @@ -214,7 +212,7 @@ class OpenGroupPoller @AssistedInject constructor( path = "/room/$room/messages/recent?t=r&reactors=5" ), endpoint = Endpoint.RoomMessagesRecent(room), - responseType = object : TypeReference>(){} + responseType = object : TypeReference>(){} ) } else { BatchRequestInfo( @@ -223,7 +221,7 @@ class OpenGroupPoller @AssistedInject constructor( path = "/room/$room/messages/since/$lastMessageServerId?t=r&reactors=5" ), endpoint = Endpoint.RoomMessagesSince(room, lastMessageServerId), - responseType = object : TypeReference>(){} + responseType = object : TypeReference>(){} ) } ) @@ -280,124 +278,104 @@ class OpenGroupPoller @AssistedInject constructor( private fun handleMessages( - server: String, roomToken: String, messages: List ) { - val sortedMessages = messages.sortedBy { it.seqno } val (deletions, additions) = messages.partition { it.deleted } - handleNewMessages(server, roomToken, additions) - handleDeletedMessages(server, roomToken, deletions.map { it.id }) - } - private suspend fun handleDirectMessages( - server: String, - fromOutbox: Boolean, - messages: List - ) { - if (messages.isEmpty()) return - val serverPublicKey = storage.getOpenGroupPublicKey(server)!! - val sortedMessages = messages.sortedBy { it.id } - val lastMessageId = sortedMessages.last().id - if (fromOutbox) { - storage.setLastOutboxMessageId(server, lastMessageId) - } else { - storage.setLastInboxMessageId(server, lastMessageId) + val threadAddress = Address.Community(serverUrl = server, room = roomToken) + // check thread still exists + val threadId = storage.getThreadId(threadAddress) ?: return + + if (additions.isNotEmpty()) { + receivedMessageProcessor.startProcessing("CommunityPoller(${threadAddress.debugString})") { ctx -> + for (msg in additions.sortedBy { it.seqno }) { + try { + // Set the last message server ID to each message as we process them, so that if processing fails halfway through, + // we don't re-process messages we've already handled. + storage.setLastMessageServerID(roomToken, server, msg.seqno) + + receivedMessageProcessor.processCommunityMessage( + context = ctx, + threadAddress = threadAddress, + message = msg, + ) + } catch (e: Exception) { + Log.e( + TAG, + "Error processing open group message ${msg.id} in ${threadAddress.debugString}", + e + ) + } + } + } + + JobQueue.shared.add(trimThreadJobFactory.create(threadId)) } - sortedMessages.forEach { - //TODO: implement direct message handling - -// val encodedMessage = Base64.decode(it.message) -// val envelope = SignalServiceProtos.Envelope.newBuilder() -// .setTimestampMs(TimeUnit.SECONDS.toMillis(it.postedAt)) -// .setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE) -// .setContent(ByteString.copyFrom(encodedMessage)) -// .setSource(it.sender) -// .build() -// try { -// val (message, proto) = messageReceiver.parse( -// envelope.toByteArray(), -// null, -// fromOutbox, -// if (fromOutbox) it.recipient else it.sender, -// serverPublicKey, -// emptySet() // this shouldn't be necessary as we are polling open groups here -// ) -// if (fromOutbox) { -// val syncTarget = blindMappingRepository.getMapping( -// serverUrl = server, -// blindedAddress = Address.Blinded(AccountId(it.recipient)) -// )?.accountId?.hexString ?: it.recipient -// -// if (message is VisibleMessage) { -// message.syncTarget = syncTarget -// } else if (message is ExpirationTimerUpdate) { -// message.syncTarget = syncTarget -// } -// } -// val threadAddress = when (val addr = message.senderOrSync.toAddress()) { -// is Address.Blinded -> Address.CommunityBlindedId(serverUrl = server, blindedId = addr) -// is Address.Conversable -> addr -// else -> throw IllegalArgumentException("Unsupported address type: ${addr.debugString}") -// } -// -// val threadId = threadDatabase.getThreadIdIfExistsFor(threadAddress) -// receivedMessageHandler.handle( -// message = message, -// proto = proto, -// threadId = threadId, -// threadAddress = threadAddress, -// ) -// } catch (e: Exception) { -// Log.e(TAG, "Couldn't handle direct message", e) -// } + if (deletions.isNotEmpty()) { + JobQueue.shared.add( + openGroupDeleteJobFactory.create( + messageServerIds = LongArray(deletions.size) { i -> deletions[i].id }, + threadId = threadId + ) + ) } } - private fun handleNewMessages(server: String, roomToken: String, sortedMessages: List) { - val threadAddress = Address.Community(serverUrl = server, room = roomToken) - // check thread still exists - val threadId = storage.getThreadId(threadAddress) ?: return - - if (sortedMessages.isEmpty()) return + /** + * Handle messages that are sent to us directly. + */ + private fun handleInboxMessages( + messages: List + ) { + if (messages.isEmpty()) return + val sorted = messages.sortedBy { it.postedAt } - receivedMessageProcessor.startProcessing("CommunityPoller(${threadAddress.debugString})") { ctx -> - for (msg in sortedMessages) { + receivedMessageProcessor.startProcessing("CommunityInbox") { ctx -> + for (apiMessage in sorted) { try { - // Set the last message server ID to each message as we process them, so that if processing fails halfway through, - // we don't re-process messages we've already handled. - storage.setLastMessageServerID(roomToken, server, msg.id) + storage.setLastInboxMessageId(server, sorted.last().id) - receivedMessageProcessor.processCommunityMessage( + receivedMessageProcessor.processCommunityInboxMessage( context = ctx, - threadAddress = threadAddress, - message = msg, + message = apiMessage, ) + } catch (e: Exception) { - Log.e(TAG, "Error processing open group message ${msg.id} in ${threadAddress.debugString}", e) + Log.e(TAG, "Error processing inbox message", e) } } } - - if (sortedMessages.isNotEmpty()) { - JobQueue.shared.add(trimThreadJobFactory.create(threadId)) - } } - private fun handleDeletedMessages(server: String, roomToken: String, serverIds: List) { - val threadID = storage.getThreadId(Address.Community(serverUrl = server, room = roomToken)) ?: return + /** + * Handle messages that we have sent out to others. + */ + private fun handleOutboxMessages( + messages: List + ) { + if (messages.isEmpty()) return + val sorted = messages.sortedBy { it.postedAt } - if (serverIds.isNotEmpty()) { - JobQueue.shared.add( - openGroupDeleteJobFactory.create( - messageServerIds = serverIds.toLongArray(), - threadId = threadID - ) - ) + receivedMessageProcessor.startProcessing("CommunityOutbox") { ctx -> + for (apiMessage in sorted) { + try { + storage.setLastOutboxMessageId(server, sorted.last().id) + + receivedMessageProcessor.processCommunityOutboxMessage( + context = ctx, + message = apiMessage, + ) + + } catch (e: Exception) { + Log.e(TAG, "Error processing inbox message", e) + } + } } } + sealed interface PollState { data class Idle(val lastPolled: Result>?) : PollState data object Polling : PollState diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a3af8339e..7b13062bc6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ kotlinVersion = "2.2.20" kryoVersion = "5.6.2" kspVersion = "2.3.0" legacySupportV13Version = "1.0.0" -libsessionUtilAndroidVersion = "1.0.9-21-g4fb53ed" +libsessionUtilAndroidVersion = "1.0.9-22-gc15b19c" media3ExoplayerVersion = "1.8.0" mockitoCoreVersion = "5.20.0" navVersion = "2.9.5" From 124395c1eabbb3dd2c6917c855214de85a11bd2b Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:51:01 +1100 Subject: [PATCH 17/19] Added comment --- .../securesms/database/ReceivedMessageHashDatabase.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt index 6494b1e50d..9436481548 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ReceivedMessageHashDatabase.kt @@ -10,6 +10,14 @@ import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton +/** + * This database table keeps track of which message hashes we've already received for + * particular senders (swarm public keys) and namespaces. This is used to prevent + * processing the same message multiple times. + * + * To use this class, call [checkOrUpdateDuplicateState] to atomically check if a message hash + * has already been seen, and if not, add it to the database. + */ @Singleton class ReceivedMessageHashDatabase @Inject constructor( @ApplicationContext context: Context, From 3fdfc2093464de1c90fa7fe19269d4d54f23a52b Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:46:28 +1100 Subject: [PATCH 18/19] PR feedback --- .../sending_receiving/MessageParser.kt | 7 ++++++- .../ReceivedMessageProcessor.kt | 8 ++----- .../sending_receiving/pollers/Poller.kt | 21 +------------------ .../securesms/groups/GroupPoller.kt | 7 +------ .../securesms/notifications/PushReceiver.kt | 4 ++-- 5 files changed, 12 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt index 91d656d266..26daf05fc4 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt @@ -133,7 +133,12 @@ class MessageParser @Inject constructor( } // Duplicate check - // TODO: Legacy code: find out why is this needed again (it was done using server hash when receiving messages) + // TODO: Legacy code: this is most likely because we try to duplicate the message we just + // send (so that a new polling won't get the same message). At the moment it's the only reliable + // way to de-duplicate sent messages as we can add the "timestamp" before hand so that when + // message arrives back from server we can identify it. The logic can be removed if we can + // calculate message hash before sending it out so we can use the existing hash de-duplication + // mechanism. if (storage.isDuplicateMessage(messageTimestampMs)) { throw NonRetryableException("Duplicate message") } diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt index d713fce7b8..bbb3d408ea 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt @@ -1,8 +1,6 @@ package org.session.libsession.messaging.sending_receiving import android.content.Context -import androidx.compose.runtime.saveable.autoSaver -import androidx.compose.ui.geometry.Rect import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -31,7 +29,6 @@ import org.session.libsession.messaging.sending_receiving.notifications.MessageN import org.session.libsession.messaging.utilities.WebRtcUtils import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address -import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID import org.session.libsession.utilities.SSKEnvironment @@ -42,7 +39,6 @@ import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.getType import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.BlindMappingRepository import org.thoughtcrime.securesms.database.RecipientRepository @@ -118,7 +114,7 @@ class ReceivedMessageProcessor @Inject constructor( } } - fun processEnvelopedMessage( + fun processSwarmMessage( context: MessageProcessingContext, threadAddress: Address.Conversable, message: Message, @@ -227,7 +223,7 @@ class ReceivedMessageProcessor @Inject constructor( currentUserId = context.currentUserId, currentUserBlindedIDs = context.getCurrentUserBlindedIDsByThread(threadAddress) )?.let { (msg, proto) -> - processEnvelopedMessage( + processSwarmMessage( context = context, threadAddress = threadAddress, message = msg, diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 1db9ee34ae..160137f53a 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -44,10 +44,8 @@ import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Snode import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase -import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager import org.thoughtcrime.securesms.util.NetworkConnectivity -import java.util.concurrent.atomic.AtomicBoolean import kotlin.time.Duration.Companion.days private const val TAG = "Poller" @@ -215,8 +213,6 @@ class Poller @AssistedInject constructor( } } - private val profiled = AtomicBoolean(true) - private fun processPersonalMessages(messages: List) { if (messages.isEmpty()) { Log.d(TAG, "No personal messages to process") @@ -225,15 +221,6 @@ class Poller @AssistedInject constructor( Log.d(TAG, "Received ${messages.size} personal messages from snode") - val start = System.currentTimeMillis() - - val shouldProfile = !profiled.getAndSet(true) - - if (shouldProfile) { - Log.d(TAG, "Start method tracing on ${Thread.currentThread().name}") - android.os.Debug.startMethodTracingSampling("${System.currentTimeMillis()}", 10 * 1024 * 1024, 1) - } - processor.startProcessing("Poller") { ctx -> for (message in messages) { if (receivedMessageHashDatabase.checkOrUpdateDuplicateState( @@ -253,7 +240,7 @@ class Poller @AssistedInject constructor( currentUserId = ctx.currentUserId ) - processor.processEnvelopedMessage( + processor.processSwarmMessage( threadAddress = message.senderOrSync.toAddress() as Address.Conversable, message = message, proto = proto, @@ -268,12 +255,6 @@ class Poller @AssistedInject constructor( } } } - - if (shouldProfile) { - android.os.Debug.stopMethodTracing() - } - - Log.d(TAG, "Processed ${messages.size} personal messages in ${System.currentTimeMillis() - start} ms") } private fun processConfig(messages: List, forConfig: UserConfigType) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index 00377a978e..76954f62e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -36,7 +36,6 @@ import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Snode import org.thoughtcrime.securesms.database.ReceivedMessageHashDatabase -import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.util.AppVisibilityManager import org.thoughtcrime.securesms.util.getRootCause import java.time.Instant @@ -55,7 +54,6 @@ class GroupPoller @AssistedInject constructor( private val receivedMessageHashDatabase: ReceivedMessageHashDatabase, private val messageParser: MessageParser, private val receivedMessageProcessor: ReceivedMessageProcessor, - private val threadDatabase: ThreadDatabase, ) { companion object { private const val POLL_INTERVAL = 3_000L @@ -456,9 +454,6 @@ class GroupPoller @AssistedInject constructor( ) } - /** - * @return The newest message handled, or null if no new messages were handled - */ private fun handleMessages(messages: List) { if (messages.isEmpty()) { return @@ -487,7 +482,7 @@ class GroupPoller @AssistedInject constructor( currentUserEd25519PrivKey = ctx.currentUserEd25519KeyPair.secretKey.data, ) - receivedMessageProcessor.processEnvelopedMessage( + receivedMessageProcessor.processSwarmMessage( threadAddress = threadAddress, message = msg, proto = proto, diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt index c80251b105..73901cd23d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -111,7 +111,7 @@ class PushReceiver @Inject constructor( currentUserEd25519PrivKey = ctx.currentUserEd25519KeyPair.secretKey.data ) - receivedMessageProcessor.processEnvelopedMessage( + receivedMessageProcessor.processSwarmMessage( threadAddress = Address.Group(groupId), message = msg, proto = proto, @@ -178,7 +178,7 @@ class PushReceiver @Inject constructor( currentUserEd25519PrivKey = ctx.currentUserEd25519KeyPair.secretKey.data, ) - receivedMessageProcessor.processEnvelopedMessage( + receivedMessageProcessor.processSwarmMessage( threadAddress = message.senderOrSync.toAddress() as Address.Conversable, message = message, proto = proto, From 841f78386029e9474d0145721cfd4aa2f1efe2f8 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:47:31 +1100 Subject: [PATCH 19/19] Remove duplication check if push notification doesn't include hash --- .../securesms/notifications/PushReceiver.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt index 73901cd23d..162c6cc8fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -156,7 +156,7 @@ class PushReceiver @Inject constructor( } namespace == Namespace.DEFAULT() || pushData?.metadata == null -> { - if (pushData?.data == null || pushData.metadata?.msg_hash == null) { + if (pushData?.data == null) { Log.d(TAG, "Push data is null") if(pushData?.metadata?.data_too_long != true) { Log.d(TAG, "Sending a generic notification (data_too_long was false)") @@ -165,15 +165,17 @@ class PushReceiver @Inject constructor( return } - if (!receivedMessageHashDatabase.checkOrUpdateDuplicateState( - swarmPublicKey = pushData.metadata.account, - namespace = Namespace.DEFAULT(), - hash = pushData.metadata.msg_hash - )) { + val isDuplicated = pushData.metadata?.msg_hash != null && receivedMessageHashDatabase.checkOrUpdateDuplicateState( + swarmPublicKey = pushData.metadata.account, + namespace = Namespace.DEFAULT(), + hash = pushData.metadata.msg_hash + ) + + if (!isDuplicated) { receivedMessageProcessor.startProcessing("PushReceiver") { ctx -> val (message, proto) = messageParser.parse1o1Message( data = pushData.data, - serverHash = pushData.metadata.msg_hash, + serverHash = pushData.metadata?.msg_hash, currentUserId = ctx.currentUserId, currentUserEd25519PrivKey = ctx.currentUserEd25519KeyPair.secretKey.data, )