diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml
index 931a654365..e9d3c6ab2f 100644
--- a/.github/workflows/build_and_test.yml
+++ b/.github/workflows/build_and_test.yml
@@ -25,7 +25,7 @@ jobs:
- variant: 'play'
run_test: true
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
submodules: 'recursive'
@@ -54,14 +54,14 @@ jobs:
- name: Upload build reports regardless
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: build-reports-${{ matrix.variant }}-${{ matrix.build_type }}
path: app/build/reports
if-no-files-found: ignore
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: session-${{ matrix.variant }}-${{ matrix.build_type }}
path: app/build/outputs/apk/${{ matrix.variant }}/${{ matrix.build_type }}/*-universal*apk
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3744e8b8f2..0db6e19270 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -26,8 +26,8 @@ configurations.configureEach {
exclude(module = "commons-logging")
}
-val canonicalVersionCode = 432
-val canonicalVersionName = "1.29.3"
+val canonicalVersionCode = 433
+val canonicalVersionName = "1.30.0"
val postFixSize = 10
val abiPostFix = mapOf(
@@ -105,7 +105,6 @@ protobuf {
android {
namespace = "network.loki.messenger"
- useLibrary("org.apache.http.legacy")
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
@@ -181,13 +180,12 @@ android {
matchingFallbacks += "release"
signingConfig = signingConfigs.getByName("debug")
- applicationIdSuffix = ".$name"
devNetDefaultOn(false)
enablePermissiveNetworkSecurityConfig(true)
setAlternativeAppName("Session QA")
- setAuthorityPostfix(".qa")
+ setAuthorityPostfix("")
}
create("automaticQa") {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0ddf065ec3..cce3a226fb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -190,9 +190,6 @@
android:screenOrientation="portrait" />
-
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..1d99e66c52 100644
--- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt
+++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt
@@ -1,6 +1,5 @@
package org.session.libsession.database
-import android.content.Context
import android.net.Uri
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.KeyPair
@@ -11,7 +10,6 @@ import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.messaging.messages.visible.Attachment
-import org.session.libsession.messaging.messages.visible.Profile
import org.session.libsession.messaging.messages.visible.Reaction
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
@@ -26,7 +24,6 @@ import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
-import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
@@ -38,12 +35,8 @@ interface StorageProtocol {
// General
fun getUserPublicKey(): String?
fun getUserED25519KeyPair(): KeyPair?
- fun getUserX25519KeyPair(): ECKeyPair
+ fun getUserX25519KeyPair(): KeyPair
fun getUserBlindedAccountId(serverPublicKey: String): AccountId?
- fun getUserProfile(): Profile
-
- // Signal
- fun getOrGenerateRegistrationID(): Int
// Jobs
fun persistJob(job: Job)
@@ -120,8 +113,6 @@ interface StorageProtocol {
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long)
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
- fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String,
- members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long): Long?
fun isLegacyClosedGroup(publicKey: String): Boolean
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
@@ -155,6 +146,8 @@ interface StorageProtocol {
fun trimThreadBefore(threadID: Long, timestamp: Long)
fun getMessageCount(threadID: Long): Long
fun getTotalPinned(): Int
+ suspend fun getTotalSentProBadges(): Int
+ suspend fun getTotalSentLongMessages(): Int
fun setPinned(address: Address, isPinned: Boolean)
fun isRead(threadId: Long) : Boolean
fun setThreadCreationDate(threadId: Long, newDate: Long)
diff --git a/app/src/main/java/org/session/libsession/messaging/file_server/FileServer.kt b/app/src/main/java/org/session/libsession/messaging/file_server/FileServer.kt
index de34f2407d..1d376ea8c9 100644
--- a/app/src/main/java/org/session/libsession/messaging/file_server/FileServer.kt
+++ b/app/src/main/java/org/session/libsession/messaging/file_server/FileServer.kt
@@ -3,11 +3,11 @@ package org.session.libsession.messaging.file_server
import kotlinx.serialization.Serializable
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
-import org.session.libsession.utilities.serializable.HttpSerializer
+import org.session.libsession.utilities.serializable.HttpUrlSerializer
@Serializable
data class FileServer(
- @Serializable(with = HttpSerializer::class)
+ @Serializable(with = HttpUrlSerializer::class)
val url: HttpUrl,
val ed25519PublicKeyHex: String
) {
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/BatchMessageReceiveJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt
index fc1bbfac42..410acddafc 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
@@ -10,7 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
-import network.loki.messenger.libsession_util.ConfigBase
+import network.loki.messenger.libsession_util.PRIORITY_HIDDEN
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message
@@ -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
@@ -62,6 +63,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 +107,7 @@ class BatchMessageReceiveJob @AssistedInject constructor(
fromCommunity = fromCommunity,
threadDatabase = threadDatabase,
recipientRepository = recipientRepository,
+ messageReceiver = messageReceiver,
)
}
@@ -141,7 +144,7 @@ class BatchMessageReceiveJob @AssistedInject constructor(
message.groupPublicKey == null && // not a group
message.openGroupServerMessageID == null && // not a community
// not marked as hidden
- configs.contacts.get(message.senderOrSync)?.priority == ConfigBase.PRIORITY_HIDDEN &&
+ configs.contacts.get(message.senderOrSync)?.priority == PRIORITY_HIDDEN &&
// the message's sentTimestamp is earlier than the sentTimestamp of the last config
message.sentTimestamp!! < contactConfigTimestamp
}
@@ -157,7 +160,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,
@@ -358,7 +361,8 @@ class BatchMessageReceiveJob @AssistedInject constructor(
@AssistedFactory
abstract class Factory : Job.DeserializeFactory {
- abstract fun create(
+ @Deprecated("New code should try to handle message directly instead of creating this job")
+ protected abstract fun create(
messages: List,
fromCommunity: Address.Community?,
): BatchMessageReceiveJob
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/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/Message.kt b/app/src/main/java/org/session/libsession/messaging/messages/Message.kt
index 223ad0ea6f..38d0c2eedc 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/Message.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/Message.kt
@@ -1,7 +1,7 @@
package org.session.libsession.messaging.messages
import network.loki.messenger.libsession_util.util.ExpiryMode
-import org.session.libsession.database.StorageProtocol
+import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.visible.VisibleMessage
@@ -48,18 +48,32 @@ abstract class Message {
&& sender != null
&& recipient != null
- abstract fun toProto(): SignalServiceProtos.Content?
+ protected abstract fun buildProto(
+ builder: SignalServiceProtos.Content.Builder,
+ messageDataProvider: MessageDataProvider
+ )
- abstract fun shouldDiscardIfBlocked(): Boolean
-
- fun SignalServiceProtos.Content.Builder.applyExpiryMode() = apply {
- expirationTimerSeconds = expiryMode.expirySeconds.toInt()
- expirationType = when (expiryMode) {
+ fun toProto(
+ builder: SignalServiceProtos.Content.Builder,
+ messageDataProvider: MessageDataProvider
+ ) {
+ // First apply common message data
+ // * Expiry mode
+ builder.expirationTimerSeconds = expiryMode.expirySeconds.toInt()
+ builder.expirationType = when (expiryMode) {
is ExpiryMode.AfterSend -> ExpirationType.DELETE_AFTER_SEND
is ExpiryMode.AfterRead -> ExpirationType.DELETE_AFTER_READ
else -> ExpirationType.UNKNOWN
}
+
+ // * Timestamps
+ builder.setSigTimestampMs(sentTimestamp!!)
+
+ // Then ask the subclasses to build their specific proto
+ buildProto(builder, messageDataProvider)
}
+
+ abstract fun shouldDiscardIfBlocked(): Boolean
}
inline fun M.copyExpiration(proto: SignalServiceProtos.Content): M = apply {
@@ -72,20 +86,10 @@ inline fun M.copyExpiration(proto: SignalServiceProtos.Cont
}
}
-fun SignalServiceProtos.Content.expiryMode(): ExpiryMode =
- (takeIf { it.hasExpirationTimerSeconds() }?.expirationTimerSeconds ?: dataMessage?.expireTimerSeconds)?.let { duration ->
- when (expirationType.takeIf { duration > 0 }) {
- ExpirationType.DELETE_AFTER_SEND -> ExpiryMode.AfterSend(duration.toLong())
- ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(duration.toLong())
- else -> ExpiryMode.NONE
- }
- } ?: ExpiryMode.NONE
-
/**
* Apply ExpiryMode from the current setting.
*/
inline fun M.applyExpiryMode(recipientAddress: Address): M = apply {
expiryMode = MessagingModuleConfiguration.shared.recipientRepository.getRecipientSync(recipientAddress)
- ?.expiryMode?.coerceSendToRead(coerceDisappearAfterSendToRead)
- ?: ExpiryMode.NONE
+ .expiryMode.coerceSendToRead(coerceDisappearAfterSendToRead)
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt b/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt
index 1ac62e171e..86a6b8ec96 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt
@@ -1,17 +1,22 @@
package org.session.libsession.messaging.messages
+import com.google.protobuf.ByteString
+import network.loki.messenger.libsession_util.protocol.ProProfileFeature
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
+import network.loki.messenger.libsession_util.util.BitSet
+import network.loki.messenger.libsession_util.util.Conversation
import network.loki.messenger.libsession_util.util.UserPic
-import org.session.libsession.messaging.messages.visible.Profile
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.updateContact
+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.RecipientSettingsDatabase
+import org.thoughtcrime.securesms.database.model.RecipientSettings
import org.thoughtcrime.securesms.util.DateUtils.Companion.secondsToInstant
import org.thoughtcrime.securesms.util.DateUtils.Companion.toEpochSeconds
import java.time.Instant
@@ -56,11 +61,14 @@ class ProfileUpdateHandler @Inject constructor(
val standardSender = unblinded ?: (senderAddress as? Address.Standard)
if (standardSender != null && (!updates.name.isNullOrBlank() || updates.pic != null)) {
configFactory.withMutableUserConfigs { configs ->
+ var shouldUpdate = false
configs.contacts.updateContact(standardSender) {
- if (shouldUpdateProfile(
+ shouldUpdate = shouldUpdateProfile(
lastUpdated = profileUpdatedEpochSeconds.secondsToInstant(),
newUpdateTime = updates.profileUpdateTime
- )) {
+ )
+
+ if (shouldUpdate) {
if (updates.name != null) {
name = updates.name
}
@@ -69,6 +77,10 @@ class ProfileUpdateHandler @Inject constructor(
profilePicture = updates.pic
}
+ if (updates.proFeatures != null) {
+ proFeatures = updates.proFeatures
+ }
+
if (updates.profileUpdateTime != null) {
profileUpdatedEpochSeconds = updates.profileUpdateTime.toEpochSeconds()
}
@@ -77,13 +89,20 @@ class ProfileUpdateHandler @Inject constructor(
Log.d(TAG, "Ignoring contact profile update for ${standardSender.debugString}, no changes detected")
}
}
+
+ if (shouldUpdate) {
+ configs.convoInfoVolatile.set(
+ configs.convoInfoVolatile.getOrConstructOneToOne(standardSender.accountId.hexString)
+ .copy(proProofInfo = updates.proProof)
+ )
+ }
}
}
// If we have a blinded address, we need to look at if we have a blinded contact to update
if (senderAddress is Address.Blinded && (updates.pic != null || !updates.name.isNullOrBlank())) {
configFactory.withMutableUserConfigs { configs ->
- configs.contacts.getBlinded(senderAddress.blindedId.hexString)?.let { c ->
+ val shouldUpdate = configs.contacts.getBlinded(senderAddress.blindedId.hexString)?.let { c ->
if (shouldUpdateProfile(
lastUpdated = c.profileUpdatedEpochSeconds.secondsToInstant(),
newUpdateTime = updates.profileUpdateTime
@@ -96,12 +115,26 @@ class ProfileUpdateHandler @Inject constructor(
c.name = updates.name
}
+ if (updates.proFeatures != null) {
+ c.proFeatures = updates.proFeatures
+ }
+
if (updates.profileUpdateTime != null) {
c.profileUpdatedEpochSeconds = updates.profileUpdateTime.toEpochSeconds()
}
configs.contacts.setBlinded(c)
+ true
+ } else {
+ false
}
+ } == true
+
+ if (shouldUpdate) {
+ configs.convoInfoVolatile.set(
+ configs.convoInfoVolatile.getOrConstructedBlindedOneToOne(senderAddress.blindedId.hexString)
+ .copy(proProofInfo = updates.proProof)
+ )
}
}
}
@@ -121,7 +154,13 @@ class ProfileUpdateHandler @Inject constructor(
r.copy(
name = updates.name ?: r.name,
profilePic = updates.pic ?: r.profilePic,
- blocksCommunityMessagesRequests = updates.blocksCommunityMessageRequests ?: r.blocksCommunityMessagesRequests
+ blocksCommunityMessagesRequests = updates.blocksCommunityMessageRequests ?: r.blocksCommunityMessagesRequests,
+ proData = updates.proProof?.let {
+ RecipientSettings.ProData(
+ info = it,
+ features = updates.proFeatures ?: BitSet()
+ )
+ },
)
} else if (updates.blocksCommunityMessageRequests != null &&
r.blocksCommunityMessagesRequests != updates.blocksCommunityMessageRequests) {
@@ -154,6 +193,8 @@ class ProfileUpdateHandler @Inject constructor(
// Name to update, must be non-blank if provided.
val name: String? = null,
val pic: UserPic? = null,
+ val proProof: Conversation.ProProofInfo? = null,
+ val proFeatures: BitSet? = null,
val blocksCommunityMessageRequests: Boolean? = null,
val profileUpdateTime: Instant?,
) {
@@ -164,44 +205,69 @@ class ProfileUpdateHandler @Inject constructor(
}
companion object {
- fun create(
- name: String? = null,
- picUrl: String?,
- picKey: ByteArray?,
- blocksCommunityMessageRequests: Boolean? = null,
- proStatus: Boolean? = null,
- profileUpdateTime: Instant?
- ): Updates? {
- val hasNameUpdate = !name.isNullOrBlank()
- val pic = when {
- picUrl == null -> null
- picUrl.isBlank() || picKey == null || picKey.size !in VALID_PROFILE_KEY_LENGTH -> UserPic.DEFAULT
- else -> UserPic(picUrl, picKey)
+ fun create(content: SignalServiceProtos.Content): Updates? {
+ val profile: SignalServiceProtos.DataMessage.LokiProfile
+ val profilePicKey: ByteString?
+
+ when {
+ content.hasDataMessage() && content.dataMessage.hasProfile() -> {
+ profile = content.dataMessage.profile
+ profilePicKey =
+ if (content.dataMessage.hasProfileKey()) content.dataMessage.profileKey else null
+ }
+
+ content.hasMessageRequestResponse() && content.messageRequestResponse.hasProfile() -> {
+ profile = content.messageRequestResponse.profile
+ profilePicKey =
+ if (content.messageRequestResponse.hasProfileKey()) content.messageRequestResponse.profileKey else null
+ }
+
+ else -> {
+ // No profile found, not updating.
+ // This is different from having an empty profile, which is a valid update.
+ return null
+ }
+ }
+
+ val pic = if (profile.hasProfilePicture()) {
+ if (!profile.profilePicture.isNullOrBlank() && profilePicKey != null &&
+ profilePicKey.size() in VALID_PROFILE_KEY_LENGTH) {
+ UserPic(
+ url = profile.profilePicture,
+ key = profilePicKey.toByteArray()
+ )
+ } else {
+ UserPic.DEFAULT // Clear the profile picture
+ }
+ } else {
+ null // No update to profile picture
}
- if (!hasNameUpdate && pic == null && blocksCommunityMessageRequests == null && proStatus == null) {
+ val name = if (profile.hasDisplayName()) profile.displayName else null
+ val blocksCommunityMessageRequests = if (content.hasDataMessage() &&
+ content.dataMessage.hasBlocksCommunityMessageRequests()) {
+ content.dataMessage.blocksCommunityMessageRequests
+ } else {
+ null
+ }
+
+ if (name == null && pic == null && blocksCommunityMessageRequests == null) {
+ // Nothing is updated..
return null
}
return Updates(
- name = if (hasNameUpdate) name else null,
+ name = name,
pic = pic,
blocksCommunityMessageRequests = blocksCommunityMessageRequests,
- profileUpdateTime = profileUpdateTime
+ profileUpdateTime = if (profile.hasLastProfileUpdateSeconds()) {
+ Instant.ofEpochSecond(profile.lastProfileUpdateSeconds)
+ } else {
+ null
+ }
)
}
- fun Profile.toUpdates(
- blocksCommunityMessageRequests: Boolean? = null,
- ): Updates? {
- return create(
- name = this.displayName,
- picUrl = this.profilePictureURL,
- picKey = this.profileKey,
- blocksCommunityMessageRequests = blocksCommunityMessageRequests,
- profileUpdateTime = this.profileUpdated
- )
- }
}
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/CallMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/CallMessage.kt
index 1d565acc62..f4c9781730 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/CallMessage.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/CallMessage.kt
@@ -1,13 +1,12 @@
package org.session.libsession.messaging.messages.control
-import org.session.libsession.messaging.messages.applyExpiryMode
+import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ANSWER
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.END_CALL
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.OFFER
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PRE_OFFER
-import org.session.libsignal.utilities.Log
import java.util.UUID
class CallMessage(): ControlMessage() {
@@ -77,23 +76,16 @@ class CallMessage(): ControlMessage() {
}
}
- override fun toProto(): SignalServiceProtos.Content? {
- val nonNullType = type ?: run {
- Log.w(TAG,"Couldn't construct call message request proto from: $this")
- return null
- }
-
- val callMessage = SignalServiceProtos.CallMessage.newBuilder()
- .setType(nonNullType)
+ protected override fun buildProto(
+ builder: SignalServiceProtos.Content.Builder,
+ messageDataProvider: MessageDataProvider
+ ) {
+ builder.callMessageBuilder
+ .setType(type!!)
.addAllSdps(sdps)
.addAllSdpMLineIndexes(sdpMLineIndexes)
.addAllSdpMids(sdpMids)
.setUuid(callId!!.toString())
-
- return SignalServiceProtos.Content.newBuilder()
- .applyExpiryMode()
- .setCallMessage(callMessage)
- .build()
}
override fun equals(other: Any?): Boolean {
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt
index 45c1b2fe3e..79872cc635 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt
@@ -1,8 +1,8 @@
package org.session.libsession.messaging.messages.control
+import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.utilities.Log
class DataExtractionNotification() : ControlMessage() {
var kind: Kind? = null
@@ -53,28 +53,17 @@ class DataExtractionNotification() : ControlMessage() {
}
}
- override fun toProto(): SignalServiceProtos.Content? {
- val kind = kind
- if (kind == null) {
- Log.w(TAG, "Couldn't construct data extraction notification proto from: $this")
- return null
- }
- try {
- val dataExtractionNotification = SignalServiceProtos.DataExtractionNotification.newBuilder()
- when(kind) {
- is Kind.Screenshot -> dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.SCREENSHOT
- is Kind.MediaSaved -> {
- dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.MEDIA_SAVED
- dataExtractionNotification.timestampMs = kind.timestamp
- }
+ protected override fun buildProto(
+ builder: SignalServiceProtos.Content.Builder,
+ messageDataProvider: MessageDataProvider
+ ) {
+ val dataExtractionNotification = builder.dataExtractionNotificationBuilder
+ when (val kind = kind!!) {
+ is Kind.Screenshot -> dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.SCREENSHOT
+ is Kind.MediaSaved -> {
+ dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.MEDIA_SAVED
+ dataExtractionNotification.timestampMs = kind.timestamp
}
- return SignalServiceProtos.Content.newBuilder()
- .setDataExtractionNotification(dataExtractionNotification.build())
- .applyExpiryMode()
- .build()
- } catch (e: Exception) {
- Log.w(TAG, "Couldn't construct data extraction notification proto from: $this")
- return null
}
}
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt
index 1e39b4d593..c7d1f21903 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt
@@ -1,10 +1,10 @@
package org.session.libsession.messaging.messages.control
+import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.copyExpiration
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
-import org.session.libsignal.utilities.Log
/** In the case of a sync message, the public key of the person the message was targeted at.
*
@@ -25,21 +25,16 @@ data class ExpirationTimerUpdate(var syncTarget: String? = null, val isGroup: Bo
}
}
- override fun toProto(): SignalServiceProtos.Content? {
- val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder().apply {
- flags = EXPIRATION_TIMER_UPDATE_VALUE
- expireTimerSeconds = expiryMode.expirySeconds.toInt()
- }
- // Sync target
- syncTarget?.let { dataMessageProto.syncTarget = it }
- return try {
- SignalServiceProtos.Content.newBuilder()
- .setDataMessage(dataMessageProto)
- .applyExpiryMode()
- .build()
- } catch (e: Exception) {
- Log.w(TAG, "Couldn't construct expiration timer update proto from: $this", e)
- null
- }
+ protected override fun buildProto(
+ builder: SignalServiceProtos.Content.Builder,
+ messageDataProvider: MessageDataProvider
+ ) {
+ builder.dataMessageBuilder
+ .setFlags(EXPIRATION_TIMER_UPDATE_VALUE)
+ .setExpireTimerSeconds(expiryMode.expirySeconds.toInt())
+ .also { builder ->
+ // Sync target
+ syncTarget?.let { builder.syncTarget = it }
+ }
}
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/GroupUpdated.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/GroupUpdated.kt
index 28120be716..16fa0cba23 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/GroupUpdated.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/GroupUpdated.kt
@@ -1,13 +1,11 @@
package org.session.libsession.messaging.messages.control
-import org.session.libsession.messaging.messages.visible.Profile
+import org.session.libsession.database.MessageDataProvider
import org.session.libsignal.protos.SignalServiceProtos.Content
-import org.session.libsignal.protos.SignalServiceProtos.DataMessage
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
class GroupUpdated @JvmOverloads constructor(
val inner: GroupUpdateMessage = GroupUpdateMessage.getDefaultInstance(),
- val profile: Profile? = null
): ControlMessage() {
override fun isValid(): Boolean {
@@ -26,18 +24,13 @@ class GroupUpdated @JvmOverloads constructor(
if (message.hasDataMessage() && message.dataMessage.hasGroupUpdateMessage())
GroupUpdated(
inner = message.dataMessage.groupUpdateMessage,
- profile = Profile.fromProto(message.dataMessage)
)
else null
}
- override fun toProto(): Content {
- val dataMessage = DataMessage.newBuilder()
+ override fun buildProto(builder: Content.Builder, messageDataProvider: MessageDataProvider) {
+ builder.dataMessageBuilder
.setGroupUpdateMessage(inner)
.apply { profile?.let(this::setProfile) }
- .build()
- return Content.newBuilder()
- .setDataMessage(dataMessage)
- .build()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt
index fad9eba2b9..bb7057b17e 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt
@@ -1,34 +1,21 @@
package org.session.libsession.messaging.messages.control
-import com.google.protobuf.ByteString
+import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
-import org.session.libsession.messaging.messages.visible.Profile
import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.utilities.Log
-class MessageRequestResponse(val isApproved: Boolean, var profile: Profile? = null) : ControlMessage() {
+class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() {
override val isSelfSendValid: Boolean = true
override fun shouldDiscardIfBlocked(): Boolean = true
- override fun toProto(): SignalServiceProtos.Content? {
- val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder()
- profile?.displayName?.let { profileProto.displayName = it }
- profile?.profilePictureURL?.let { profileProto.profilePicture = it }
- val messageRequestResponseProto = SignalServiceProtos.MessageRequestResponse.newBuilder()
+ override fun buildProto(
+ builder: SignalServiceProtos.Content.Builder,
+ messageDataProvider: MessageDataProvider
+ ) {
+ builder.messageRequestResponseBuilder
.setIsApproved(isApproved)
- .setProfile(profileProto.build())
- profile?.profileKey?.let { messageRequestResponseProto.profileKey = ByteString.copyFrom(it) }
- return try {
- SignalServiceProtos.Content.newBuilder()
- .applyExpiryMode()
- .setMessageRequestResponse(messageRequestResponseProto.build())
- .build()
- } catch (e: Exception) {
- Log.w(TAG, "Couldn't construct message request response proto from: $this")
- null
- }
}
companion object {
@@ -37,13 +24,7 @@ class MessageRequestResponse(val isApproved: Boolean, var profile: Profile? = nu
fun fromProto(proto: SignalServiceProtos.Content): MessageRequestResponse? {
val messageRequestResponseProto = if (proto.hasMessageRequestResponse()) proto.messageRequestResponse else return null
val isApproved = messageRequestResponseProto.isApproved
- val profileProto = messageRequestResponseProto.profile
- val profile = Profile().apply {
- displayName = profileProto.displayName
- profileKey = if (messageRequestResponseProto.hasProfileKey()) messageRequestResponseProto.profileKey.toByteArray() else null
- profilePictureURL = profileProto.profilePicture
- }
- return MessageRequestResponse(isApproved, profile)
+ return MessageRequestResponse(isApproved)
.copyExpiration(proto)
}
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt
index 876f47dd33..4c8021e416 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt
@@ -1,8 +1,8 @@
package org.session.libsession.messaging.messages.control
+import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.utilities.Log
class ReadReceipt() : ControlMessage() {
var timestamps: List? = null
@@ -33,24 +33,15 @@ class ReadReceipt() : ControlMessage() {
this.timestamps = timestamps
}
- override fun toProto(): SignalServiceProtos.Content? {
- val timestamps = timestamps
- if (timestamps == null) {
- Log.w(TAG, "Couldn't construct read receipt proto from: $this")
- return null
- }
-
- return try {
- SignalServiceProtos.Content.newBuilder()
- .setReceiptMessage(
- SignalServiceProtos.ReceiptMessage.newBuilder()
- .setType(SignalServiceProtos.ReceiptMessage.Type.READ)
- .addAllTimestampMs(timestamps.asIterable()).build()
- ).applyExpiryMode()
- .build()
- } catch (e: Exception) {
- Log.w(TAG, "Couldn't construct read receipt proto from: $this")
- null
- }
+ protected override fun buildProto(
+ builder: SignalServiceProtos.Content.Builder,
+ messageDataProvider: MessageDataProvider
+ ) {
+ builder
+ .receiptMessageBuilder
+ .setType(SignalServiceProtos.ReceiptMessage.Type.READ)
+ .addAllTimestampMs(requireNotNull(timestamps) {
+ "Timestamps is null"
+ })
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt
index 92a172e9e6..ab2c17a1f2 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt
@@ -1,8 +1,8 @@
package org.session.libsession.messaging.messages.control
+import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.utilities.Log
class TypingIndicator() : ControlMessage() {
var kind: Kind? = null
@@ -51,21 +51,12 @@ class TypingIndicator() : ControlMessage() {
this.kind = kind
}
- override fun toProto(): SignalServiceProtos.Content? {
- val timestamp = sentTimestamp
- val kind = kind
- if (timestamp == null || kind == null) {
- Log.w(TAG, "Couldn't construct typing indicator proto from: $this")
- return null
- }
- return try {
- SignalServiceProtos.Content.newBuilder()
- .setTypingMessage(SignalServiceProtos.TypingMessage.newBuilder().setTimestampMs(timestamp).setAction(kind.toProto()).build())
- .applyExpiryMode()
- .build()
- } catch (e: Exception) {
- Log.w(TAG, "Couldn't construct typing indicator proto from: $this")
- null
- }
+ protected override fun buildProto(
+ builder: SignalServiceProtos.Content.Builder,
+ messageDataProvider: MessageDataProvider
+ ) {
+ builder.typingMessageBuilder
+ .setTimestampMs(sentTimestamp!!)
+ .setAction(kind!!.toProto())
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/UnsendRequest.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/UnsendRequest.kt
index 6e04375eda..38f5d7d34b 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/UnsendRequest.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/UnsendRequest.kt
@@ -1,8 +1,8 @@
package org.session.libsession.messaging.messages.control
+import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.utilities.Log
class UnsendRequest(var timestamp: Long? = null, var author: String? = null): ControlMessage() {
@@ -24,22 +24,13 @@ class UnsendRequest(var timestamp: Long? = null, var author: String? = null): Co
proto.takeIf { it.hasUnsendRequest() }?.unsendRequest?.run { UnsendRequest(timestampMs, author) }?.copyExpiration(proto)
}
- override fun toProto(): SignalServiceProtos.Content? {
- val timestamp = timestamp
- val author = author
- if (timestamp == null || author == null) {
- Log.w(TAG, "Couldn't construct unsend request proto from: $this")
- return null
- }
- return try {
- SignalServiceProtos.Content.newBuilder()
- .setUnsendRequest(SignalServiceProtos.UnsendRequest.newBuilder().setTimestampMs(timestamp).setAuthor(author).build())
- .applyExpiryMode()
- .build()
- } catch (e: Exception) {
- Log.w(TAG, "Couldn't construct unsend request proto from: $this")
- null
- }
+ protected override fun buildProto(
+ builder: SignalServiceProtos.Content.Builder,
+ messageDataProvider: MessageDataProvider
+ ) {
+ builder.unsendRequestBuilder
+ .setTimestampMs(timestamp!!)
+ .setAuthor(author!!)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingEncryptedMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingEncryptedMessage.java
deleted file mode 100644
index f5a63d4ac5..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingEncryptedMessage.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.session.libsession.messaging.messages.signal;
-
-public class IncomingEncryptedMessage extends IncomingTextMessage {
-
- public IncomingEncryptedMessage(IncomingTextMessage base, String newBody) {
- super(base, newBody);
- }
-
- @Override
- public boolean isSecureMessage() {
- return true;
- }
-}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingGroupMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingGroupMessage.java
deleted file mode 100644
index 8046c82d99..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingGroupMessage.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.session.libsession.messaging.messages.signal;
-
-public class IncomingGroupMessage extends IncomingTextMessage {
-
- private final boolean updateMessage;
-
- public IncomingGroupMessage(IncomingTextMessage base, String body, boolean updateMessage) {
- super(base, body);
- this.updateMessage = updateMessage;
- }
-
- @Override
- public boolean isGroup() {
- return true;
- }
-
- public boolean isUpdateMessage() { return updateMessage; }
-
-}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java
deleted file mode 100644
index 3e040232eb..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java
+++ /dev/null
@@ -1,168 +0,0 @@
-package org.session.libsession.messaging.messages.signal;
-
-import org.jspecify.annotations.Nullable;
-import org.session.libsession.messaging.messages.visible.VisibleMessage;
-import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
-import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
-import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
-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.Contact;
-import org.session.libsignal.messages.SignalServiceAttachment;
-import org.session.libsignal.utilities.guava.Optional;
-import org.thoughtcrime.securesms.database.model.content.MessageContent;
-
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-
-public class IncomingMediaMessage {
-
- private final Address from;
- private final Address.GroupLike groupId;
- private final String body;
- private final boolean push;
- private final long sentTimeMillis;
- private final int subscriptionId;
- private final long expiresIn;
- private final long expireStartedAt;
- private final boolean messageRequestResponse;
- private final boolean hasMention;
- @Nullable
- private final MessageContent messageContent;
-
- private final DataExtractionNotificationInfoMessage dataExtractionNotification;
- private final QuoteModel quote;
-
- private final List attachments = new LinkedList<>();
- private final List sharedContacts = new LinkedList<>();
- private final List linkPreviews = new LinkedList<>();
-
- public IncomingMediaMessage(Address from,
- long sentTimeMillis,
- int subscriptionId,
- long expiresIn,
- long expireStartedAt,
- boolean messageRequestResponse,
- boolean hasMention,
- Optional body,
- Optional group,
- Optional> attachments,
- @Nullable MessageContent messageContent,
- Optional quote,
- Optional> sharedContacts,
- Optional> linkPreviews,
- Optional dataExtractionNotification)
- {
- this.messageContent = messageContent;
- this.push = true;
- this.from = from;
- this.sentTimeMillis = sentTimeMillis;
- this.body = body.orNull();
- this.subscriptionId = subscriptionId;
- this.expiresIn = expiresIn;
- this.expireStartedAt = expireStartedAt;
- this.dataExtractionNotification = dataExtractionNotification.orNull();
- this.quote = quote.orNull();
- this.messageRequestResponse = messageRequestResponse;
- this.hasMention = hasMention;
- this.groupId = group.orNull();
-
- this.attachments.addAll(PointerAttachment.forPointers(attachments));
- this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
- this.linkPreviews.addAll(linkPreviews.or(Collections.emptyList()));
- }
-
- public static IncomingMediaMessage from(VisibleMessage message,
- Address from,
- long expiresIn,
- long expireStartedAt,
- Optional group,
- List attachments,
- Optional quote,
- Optional> linkPreviews)
- {
- return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, expireStartedAt,
- false, message.getHasMention(), Optional.fromNullable(message.getText()),
- group, Optional.fromNullable(attachments), null, quote, Optional.absent(), linkPreviews, Optional.absent());
- }
-
- public int getSubscriptionId() {
- return subscriptionId;
- }
-
- public String getBody() {
- return body;
- }
-
- public List getAttachments() {
- return attachments;
- }
-
- public Address getFrom() {
- return from;
- }
-
- public Address.GroupLike getGroupId() {
- return groupId;
- }
-
- public @Nullable MessageContent getMessageContent() {
- return messageContent;
- }
-
- public boolean isPushMessage() {
- return push;
- }
-
- public long getSentTimeMillis() {
- return sentTimeMillis;
- }
-
- public long getExpiresIn() {
- return expiresIn;
- }
-
- public long getExpireStartedAt() {
- return expireStartedAt;
- }
-
- public boolean isGroupMessage() {
- return groupId != null;
- }
-
- public boolean hasMention() {
- return hasMention;
- }
-
- public boolean isScreenshotDataExtraction() {
- if (dataExtractionNotification == null) return false;
- else {
- return dataExtractionNotification.getKind() == DataExtractionNotificationInfoMessage.Kind.SCREENSHOT;
- }
- }
-
- public boolean isMediaSavedDataExtraction() {
- if (dataExtractionNotification == null) return false;
- else {
- return dataExtractionNotification.getKind() == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED;
- }
- }
-
- public QuoteModel getQuote() {
- return quote;
- }
-
- public List getSharedContacts() {
- return sharedContacts;
- }
-
- public List getLinkPreviews() {
- return linkPreviews;
- }
-
- public boolean isMessageRequestResponse() {
- return messageRequestResponse;
- }
-}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.kt
new file mode 100644
index 0000000000..adf9331452
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.kt
@@ -0,0 +1,57 @@
+package org.session.libsession.messaging.messages.signal
+
+import network.loki.messenger.libsession_util.protocol.ProFeature
+import org.session.libsession.messaging.messages.visible.VisibleMessage
+import org.session.libsession.messaging.sending_receiving.attachments.Attachment
+import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
+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.thoughtcrime.securesms.database.model.content.MessageContent
+
+class IncomingMediaMessage(
+ val from: Address,
+ val sentTimeMillis: Long,
+ val expiresIn: Long,
+ val expireStartedAt: Long,
+ val isMessageRequestResponse: Boolean,
+ val hasMention: Boolean,
+ val body: String?,
+ val group: Address.GroupLike?,
+ val attachments: List,
+ val proFeatures: Set,
+ val messageContent: MessageContent?,
+ val quote: QuoteModel?,
+ val linkPreviews: List,
+ val dataExtractionNotification: DataExtractionNotificationInfoMessage?,
+) {
+
+ constructor(
+ message: VisibleMessage,
+ from: Address,
+ expiresIn: Long,
+ expireStartedAt: Long,
+ group: Address.GroupLike?,
+ attachments: List,
+ quote: QuoteModel?,
+ linkPreviews: List
+ ): this(
+ from = from,
+ sentTimeMillis = message.sentTimestamp!!,
+ expiresIn = expiresIn,
+ expireStartedAt = expireStartedAt,
+ isMessageRequestResponse = false,
+ hasMention = message.hasMention,
+ body = message.text,
+ group = group,
+ attachments = attachments,
+ proFeatures = message.proFeatures,
+ messageContent = null,
+ quote = quote,
+ linkPreviews = linkPreviews,
+ dataExtractionNotification = null
+ )
+
+ val isMediaSavedDataExtraction: Boolean get() =
+ dataExtractionNotification?.kind == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.java
deleted file mode 100644
index 73bfd7a4d4..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.java
+++ /dev/null
@@ -1,257 +0,0 @@
-package org.session.libsession.messaging.messages.signal;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import androidx.annotation.Nullable;
-
-import org.session.libsession.messaging.calls.CallMessageType;
-import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
-import org.session.libsession.messaging.messages.visible.VisibleMessage;
-import org.session.libsession.messaging.utilities.UpdateMessageData;
-import org.session.libsession.utilities.Address;
-import org.session.libsignal.utilities.guava.Optional;
-
-public class IncomingTextMessage implements Parcelable {
-
- public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
- @Override
- public IncomingTextMessage createFromParcel(Parcel in) {
- return new IncomingTextMessage(in);
- }
-
- @Override
- public IncomingTextMessage[] newArray(int size) {
- return new IncomingTextMessage[size];
- }
- };
- private static final String TAG = IncomingTextMessage.class.getSimpleName();
-
- private final String message;
- private Address sender;
- private final int senderDeviceId;
- private final int protocol;
- private final String serviceCenterAddress;
- private final boolean replyPathPresent;
- private final String pseudoSubject;
- private final long sentTimestampMillis;
- private final Address.GroupLike groupId;
- private final boolean push;
- private final int subscriptionId;
- private final long expiresInMillis;
- private final long expireStartedAt;
- private final boolean unidentified;
- private final int callType;
- private final boolean hasMention;
-
- private boolean isOpenGroupInvitation = false;
-
- public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
- String encodedBody, Optional group,
- long expiresInMillis, long expireStartedAt, boolean unidentified, boolean hasMention) {
- this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, expireStartedAt, unidentified, -1, hasMention);
- }
-
- public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
- String encodedBody, Optional group,
- long expiresInMillis, long expireStartedAt, boolean unidentified, int callType, boolean hasMention) {
- this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, expireStartedAt, unidentified, callType, hasMention, true);
- }
-
- public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
- String encodedBody, Optional group,
- long expiresInMillis, long expireStartedAt, boolean unidentified, int callType, boolean hasMention, boolean isPush) {
- this.message = encodedBody;
- this.sender = sender;
- this.senderDeviceId = senderDeviceId;
- this.protocol = 31337;
- this.serviceCenterAddress = "GCM";
- this.replyPathPresent = true;
- this.pseudoSubject = "";
- this.sentTimestampMillis = sentTimestampMillis;
- this.push = isPush;
- this.subscriptionId = -1;
- this.expiresInMillis = expiresInMillis;
- this.expireStartedAt = expireStartedAt;
- this.unidentified = unidentified;
- this.callType = callType;
- this.hasMention = hasMention;
- this.groupId = group.orNull();
- }
-
- public IncomingTextMessage(Parcel in) {
- this.message = in.readString();
- this.sender = in.readParcelable(IncomingTextMessage.class.getClassLoader());
- this.senderDeviceId = in.readInt();
- this.protocol = in.readInt();
- this.serviceCenterAddress = in.readString();
- this.replyPathPresent = (in.readInt() == 1);
- this.pseudoSubject = in.readString();
- this.sentTimestampMillis = in.readLong();
- this.groupId = in.readParcelable(IncomingTextMessage.class.getClassLoader());
- this.push = (in.readInt() == 1);
- this.subscriptionId = in.readInt();
- this.expiresInMillis = in.readLong();
- this.expireStartedAt = in.readLong();
- this.unidentified = in.readInt() == 1;
- this.isOpenGroupInvitation = in.readInt() == 1;
- this.callType = in.readInt();
- this.hasMention = in.readInt() == 1;
- }
-
- public IncomingTextMessage(IncomingTextMessage base, String newBody) {
- this.message = newBody;
- this.sender = base.getSender();
- this.senderDeviceId = base.getSenderDeviceId();
- this.protocol = base.getProtocol();
- this.serviceCenterAddress = base.getServiceCenterAddress();
- this.replyPathPresent = base.isReplyPathPresent();
- this.pseudoSubject = base.getPseudoSubject();
- this.sentTimestampMillis = base.getSentTimestampMillis();
- this.groupId = base.getGroupId();
- this.push = base.isPush();
- this.subscriptionId = base.getSubscriptionId();
- this.expiresInMillis = base.getExpiresIn();
- this.expireStartedAt = base.getExpireStartedAt();
- this.unidentified = base.isUnidentified();
- this.isOpenGroupInvitation = base.isOpenGroupInvitation();
- this.callType = base.callType;
- this.hasMention = base.hasMention;
- }
-
- public static IncomingTextMessage from(VisibleMessage message,
- Address sender,
- Optional group,
- long expiresInMillis,
- long expireStartedAt)
- {
- return new IncomingTextMessage(sender, 1, message.getSentTimestamp(), message.getText(), group, expiresInMillis, expireStartedAt, false, message.getHasMention());
- }
-
- public static IncomingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation,
- Address sender,
- Long sentTimestamp,
- long expiresInMillis,
- long expireStartedAt) {
- String url = openGroupInvitation.getUrl();
- String name = openGroupInvitation.getName();
- if (url == null || name == null) { return null; }
- // FIXME: Doing toJSON() to get the body here is weird
- String body = UpdateMessageData.Companion.buildOpenGroupInvitation(url, name).toJSON();
- IncomingTextMessage incomingTextMessage = new IncomingTextMessage(sender, 1, sentTimestamp, body, Optional.absent(), expiresInMillis, expireStartedAt, false, false);
- incomingTextMessage.isOpenGroupInvitation = true;
- return incomingTextMessage;
- }
-
- public static IncomingTextMessage fromCallInfo(CallMessageType callMessageType,
- Address sender,
- Optional group,
- long sentTimestamp,
- long expiresInMillis,
- long expireStartedAt) {
- return new IncomingTextMessage(sender, 1, sentTimestamp, null, group, expiresInMillis, expireStartedAt, false, callMessageType.ordinal(), false, false);
- }
-
- public int getSubscriptionId() {
- return subscriptionId;
- }
-
- public long getExpiresIn() {
- return expiresInMillis;
- }
-
- public long getExpireStartedAt() {
- return expireStartedAt;
- }
-
- public long getSentTimestampMillis() {
- return sentTimestampMillis;
- }
-
- public String getPseudoSubject() {
- return pseudoSubject;
- }
-
- public String getMessageBody() {
- return message;
- }
-
- public Address getSender() {
- return sender;
- }
-
- public int getSenderDeviceId() {
- return senderDeviceId;
- }
-
- public int getProtocol() {
- return protocol;
- }
-
- public String getServiceCenterAddress() {
- return serviceCenterAddress;
- }
-
- public boolean isReplyPathPresent() {
- return replyPathPresent;
- }
-
- public boolean isSecureMessage() {
- return false;
- }
-
- public boolean isPush() {
- return push;
- }
-
- public @Nullable Address.GroupLike getGroupId() {
- return groupId;
- }
-
- public boolean isGroup() {
- return false;
- }
-
- public boolean isUnidentified() {
- return unidentified;
- }
-
- public boolean isOpenGroupInvitation() { return isOpenGroupInvitation; }
-
- public boolean hasMention() { return hasMention; }
-
- public boolean isUnreadCallMessage() {
- return callType == CallMessageType.CALL_MISSED.ordinal() || callType == CallMessageType.CALL_FIRST_MISSED.ordinal();
- }
-
- @Nullable
- public CallMessageType getCallType() {
- int callTypeLength = CallMessageType.values().length;
- if (callType < 0 || callType >= callTypeLength) return null;
- return CallMessageType.values()[callType];
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeString(message);
- out.writeParcelable(sender, flags);
- out.writeInt(senderDeviceId);
- out.writeInt(protocol);
- out.writeString(serviceCenterAddress);
- out.writeInt(replyPathPresent ? 1 : 0);
- out.writeString(pseudoSubject);
- out.writeLong(sentTimestampMillis);
- out.writeParcelable(groupId, flags);
- out.writeInt(push ? 1 : 0);
- out.writeInt(subscriptionId);
- out.writeInt(unidentified ? 1 : 0);
- out.writeInt(isOpenGroupInvitation ? 1 : 0);
- out.writeInt(callType);
- out.writeInt(hasMention ? 1 : 0);
- }
-}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt
new file mode 100644
index 0000000000..3d3be4641e
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt
@@ -0,0 +1,113 @@
+package org.session.libsession.messaging.messages.signal
+
+import network.loki.messenger.libsession_util.protocol.ProFeature
+import org.session.libsession.messaging.calls.CallMessageType
+import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
+import org.session.libsession.messaging.messages.visible.VisibleMessage
+import org.session.libsession.messaging.utilities.UpdateMessageData
+import org.session.libsession.utilities.Address
+import java.util.EnumSet
+
+data class IncomingTextMessage(
+ val message: String?,
+ val sender: Address,
+ val sentTimestampMillis: Long,
+ val group: Address.GroupLike?,
+ val push: Boolean,
+ val expiresInMillis: Long,
+ val expireStartedAt: Long,
+ val callType: Int,
+ val hasMention: Boolean,
+ val isOpenGroupInvitation: Boolean,
+ val isSecureMessage: Boolean,
+ val proFeatures: Set,
+ val isGroupMessage: Boolean = false,
+ val isGroupUpdateMessage: Boolean = false,
+) {
+ val callMessageType: CallMessageType? get() =
+ CallMessageType.entries.getOrNull(callType)
+
+ val isUnreadCallMessage: Boolean
+ get() = callMessageType in EnumSet.of(
+ CallMessageType.CALL_MISSED,
+ CallMessageType.CALL_FIRST_MISSED,
+ )
+
+ init {
+ check(!isGroupUpdateMessage || isGroupMessage) {
+ "A message cannot be a group update message if it is not a group message"
+ }
+ }
+
+ constructor(
+ message: VisibleMessage,
+ sender: Address,
+ group: Address.GroupLike?,
+ expiresInMillis: Long,
+ expireStartedAt: Long,
+ ): this(
+ message = message.text,
+ sender = sender,
+ sentTimestampMillis = message.sentTimestamp!!,
+ group = group,
+ push = true,
+ expiresInMillis = expiresInMillis,
+ expireStartedAt = expireStartedAt,
+ callType = -1,
+ hasMention = message.hasMention,
+ isOpenGroupInvitation = false,
+ isSecureMessage = false,
+ proFeatures = message.proFeatures,
+ )
+ constructor(
+ callMessageType: CallMessageType,
+ sender: Address,
+ group: Address.GroupLike?,
+ sentTimestampMillis: Long,
+ expiresInMillis: Long,
+ expireStartedAt: Long,
+ ): this(
+ message = null,
+ sender = sender,
+ sentTimestampMillis = sentTimestampMillis,
+ group = group,
+ push = false,
+ expiresInMillis = expiresInMillis,
+ expireStartedAt = expireStartedAt,
+ callType = callMessageType.ordinal,
+ hasMention = false,
+ isOpenGroupInvitation = false,
+ isSecureMessage = false,
+ proFeatures = emptySet(),
+ )
+
+ companion object {
+ fun fromOpenGroupInvitation(
+ invitation: OpenGroupInvitation,
+ sender: Address,
+ sentTimestampMillis: Long,
+ expiresInMillis: Long,
+ expireStartedAt: Long,
+ ): IncomingTextMessage? {
+ val body = UpdateMessageData.buildOpenGroupInvitation(
+ url = invitation.url ?: return null,
+ name = invitation.name ?: return null,
+ ).toJSON()
+
+ return IncomingTextMessage(
+ message = body,
+ sender = sender,
+ sentTimestampMillis = sentTimestampMillis,
+ group = null,
+ push = true,
+ expiresInMillis = expiresInMillis,
+ expireStartedAt = expireStartedAt,
+ callType = -1,
+ hasMention = false,
+ isOpenGroupInvitation = true,
+ isSecureMessage = false,
+ proFeatures = emptySet(),
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java
deleted file mode 100644
index 05ed23dbaf..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.session.libsession.messaging.messages.signal;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.session.libsession.utilities.Address;
-import org.session.libsession.utilities.DistributionTypes;
-import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
-import org.session.libsession.utilities.Contact;
-import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
-import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
-import org.thoughtcrime.securesms.database.model.content.MessageContent;
-
-import java.util.LinkedList;
-import java.util.List;
-
-public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
-
- private final String groupID;
- private final boolean isUpdateMessage;
-
- public OutgoingGroupMediaMessage(@NonNull Address recipient,
- @NonNull String body,
- @Nullable String groupId,
- @Nullable final Attachment avatar,
- long sentTime,
- long expireIn,
- long expireStartedAt,
- boolean updateMessage,
- @Nullable QuoteModel quote,
- @NonNull List contacts,
- @NonNull List previews,
- @Nullable MessageContent messageContent)
- {
- super(recipient, body,
- new LinkedList() {{if (avatar != null) add(avatar);}},
- sentTime,
- DistributionTypes.CONVERSATION, expireIn, expireStartedAt, quote, contacts, previews, messageContent);
-
- this.groupID = groupId;
- this.isUpdateMessage = updateMessage;
- }
-
- @Override
- public boolean isGroup() {
- return true;
- }
-
- public String getGroupId() {
- return groupID;
- }
-
- public boolean isUpdateMessage() {
- return isUpdateMessage;
- }
-}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java
deleted file mode 100644
index 67c35f699c..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java
+++ /dev/null
@@ -1,163 +0,0 @@
-package org.session.libsession.messaging.messages.signal;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.session.libsession.messaging.messages.visible.VisibleMessage;
-import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
-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.Contact;
-import org.session.libsession.utilities.DistributionTypes;
-import org.session.libsession.utilities.IdentityKeyMismatch;
-import org.session.libsession.utilities.NetworkFailure;
-import org.thoughtcrime.securesms.database.model.content.MessageContent;
-
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * Represents an outgoing mms message. Note this class is only used for saving messages
- * into the database. We will still use {@link org.session.libsession.messaging.messages.Message}
- * as a model when sending the message to the network.
- *
- * See {@link OutgoingTextMessage} for the sms table counterpart.
- */
-public class OutgoingMediaMessage {
-
- private final Address recipient;
- protected final String body;
- protected final List attachments;
- private final long sentTimeMillis;
- private final int distributionType;
- private final int subscriptionId;
- private final long expiresIn;
- private final long expireStartedAt;
- private final QuoteModel outgoingQuote;
- @Nullable
- private final MessageContent messageContent;
-
- private final List networkFailures = new LinkedList<>();
- private final List identityKeyMismatches = new LinkedList<>();
- private final List contacts = new LinkedList<>();
- private final List linkPreviews = new LinkedList<>();
-
- public OutgoingMediaMessage(Address recipient, String message,
- List attachments, long sentTimeMillis,
- int subscriptionId, long expiresIn, long expireStartedAt,
- int distributionType,
- @Nullable QuoteModel outgoingQuote,
- @NonNull List contacts,
- @NonNull List linkPreviews,
- @NonNull List networkFailures,
- @NonNull List identityKeyMismatches,
- @Nullable MessageContent messageContent)
- {
- this.recipient = recipient;
- this.body = message;
- this.sentTimeMillis = sentTimeMillis;
- this.distributionType = distributionType;
- this.attachments = attachments;
- this.subscriptionId = subscriptionId;
- this.expiresIn = expiresIn;
- this.expireStartedAt = expireStartedAt;
- this.outgoingQuote = outgoingQuote;
- this.messageContent = messageContent;
-
- this.contacts.addAll(contacts);
- this.linkPreviews.addAll(linkPreviews);
- this.networkFailures.addAll(networkFailures);
- this.identityKeyMismatches.addAll(identityKeyMismatches);
- }
-
- public OutgoingMediaMessage(OutgoingMediaMessage that) {
- this.recipient = that.getRecipient();
- this.body = that.body;
- this.distributionType = that.distributionType;
- this.attachments = that.attachments;
- this.sentTimeMillis = that.sentTimeMillis;
- this.subscriptionId = that.subscriptionId;
- this.expiresIn = that.expiresIn;
- this.expireStartedAt = that.expireStartedAt;
- this.outgoingQuote = that.outgoingQuote;
- this.messageContent = that.messageContent;
-
- this.identityKeyMismatches.addAll(that.identityKeyMismatches);
- this.networkFailures.addAll(that.networkFailures);
- this.contacts.addAll(that.contacts);
- this.linkPreviews.addAll(that.linkPreviews);
- }
-
- public static OutgoingMediaMessage from(VisibleMessage message,
- Address recipient,
- List attachments,
- @Nullable QuoteModel outgoingQuote,
- @Nullable LinkPreview linkPreview,
- long expiresInMillis,
- long expireStartedAt)
- {
- List previews = Collections.emptyList();
- if (linkPreview != null) {
- previews = Collections.singletonList(linkPreview);
- }
- return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
- expiresInMillis, expireStartedAt, DistributionTypes.DEFAULT, outgoingQuote,
- Collections.emptyList(), previews, Collections.emptyList(), Collections.emptyList(), null);
- }
-
- @Nullable
- public MessageContent getMessageContent() {
- return messageContent;
- }
-
- public Address getRecipient() {
- return recipient;
- }
-
- public String getBody() {
- return body;
- }
-
- public List getAttachments() {
- return attachments;
- }
-
- public boolean isSecure() {
- return true;
- }
-
- public boolean isGroup() {
- return false;
- }
-
- public long getSentTimeMillis() {
- return sentTimeMillis;
- }
-
- public int getSubscriptionId() {
- return subscriptionId;
- }
-
- public long getExpiresIn() {
- return expiresIn;
- }
-
- public long getExpireStartedAt() {
- return expireStartedAt;
- }
-
- public @Nullable QuoteModel getOutgoingQuote() {
- return outgoingQuote;
- }
-
- public @NonNull List getSharedContacts() {
- return contacts;
- }
-
- public @NonNull List getLinkPreviews() {
- return linkPreviews;
- }
-
-}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.kt
new file mode 100644
index 0000000000..b7183cd8c8
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.kt
@@ -0,0 +1,83 @@
+package org.session.libsession.messaging.messages.signal
+
+import network.loki.messenger.libsession_util.protocol.ProFeature
+import org.session.libsession.messaging.messages.visible.VisibleMessage
+import org.session.libsession.messaging.sending_receiving.attachments.Attachment
+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.thoughtcrime.securesms.database.model.content.MessageContent
+
+class OutgoingMediaMessage(
+ val recipient: Address,
+ val body: String?,
+ val attachments: List,
+ val sentTimeMillis: Long,
+ val expiresInMillis: Long,
+ val expireStartedAtMillis: Long,
+ val outgoingQuote: QuoteModel?,
+ val messageContent: MessageContent?,
+ val linkPreviews: List,
+ val group: Address.GroupLike?,
+ val isGroupUpdateMessage: Boolean,
+ val proFeatures: Set = emptySet()
+) {
+ init {
+ check(!isGroupUpdateMessage || group != null) {
+ "Group update messages must have a group address"
+ }
+ }
+
+ constructor(
+ message: VisibleMessage,
+ recipient: Address,
+ attachments: List,
+ outgoingQuote: QuoteModel?,
+ linkPreview: LinkPreview?,
+ expiresInMillis: Long,
+ expireStartedAt: Long
+ ) : this(
+ recipient = recipient,
+ body = message.text,
+ attachments = attachments,
+ sentTimeMillis = message.sentTimestamp!!,
+ expiresInMillis = expiresInMillis,
+ expireStartedAtMillis = expireStartedAt,
+ outgoingQuote = outgoingQuote,
+ messageContent = null,
+ linkPreviews = linkPreview?.let { listOf(it) } ?: emptyList(),
+ group = null,
+ isGroupUpdateMessage = false,
+ )
+
+ constructor(
+ recipient: Address,
+ body: String?,
+ group: Address.GroupLike,
+ avatar: Attachment?,
+ sentTimeMillis: Long,
+ expiresInMillis: Long,
+ expireStartedAtMillis: Long,
+ isGroupUpdateMessage: Boolean,
+ quote: QuoteModel?,
+ previews: List,
+ messageContent: MessageContent?,
+ ) : this(
+ recipient = recipient,
+ body = body,
+ attachments = avatar?.let { listOf(it) } ?: emptyList(),
+ sentTimeMillis = sentTimeMillis,
+ expiresInMillis = expiresInMillis,
+ expireStartedAtMillis = expireStartedAtMillis,
+ outgoingQuote = quote,
+ messageContent = messageContent,
+ linkPreviews = previews,
+ group = group,
+ isGroupUpdateMessage = isGroupUpdateMessage,
+ )
+
+ // legacy code
+ val isSecure: Boolean get() = true
+
+ val isGroup: Boolean get() = group != null
+}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java
deleted file mode 100644
index 0f594d4eba..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.session.libsession.messaging.messages.signal;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
-import org.session.libsession.utilities.Address;
-import org.session.libsession.utilities.Contact;
-import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
-import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
-import org.thoughtcrime.securesms.database.model.content.MessageContent;
-
-import java.util.Collections;
-import java.util.List;
-
-public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
-
- public OutgoingSecureMediaMessage(Address recipient, String body,
- List attachments,
- long sentTimeMillis,
- int distributionType,
- long expiresIn,
- long expireStartedAt,
- @Nullable QuoteModel quote,
- @NonNull List contacts,
- @NonNull List previews,
- @Nullable MessageContent messageContent)
- {
- super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, expireStartedAt, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList(), messageContent);
- }
-
- public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
- super(base);
- }
-
- @Override
- public boolean isSecure() {
- return true;
- }
-}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.java b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.java
deleted file mode 100644
index c79a68102f..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package org.session.libsession.messaging.messages.signal;
-
-import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
-import org.session.libsession.messaging.messages.visible.VisibleMessage;
-import org.session.libsession.utilities.Address;
-import org.session.libsession.messaging.utilities.UpdateMessageData;
-
-public class OutgoingTextMessage {
- private final Address recipient;
- private final String message;
- private final int subscriptionId;
- private final long expiresIn;
- private final long expireStartedAt;
- private final long sentTimestampMillis;
- private boolean isOpenGroupInvitation = false;
-
- public OutgoingTextMessage(Address recipient, String message, long expiresIn, long expireStartedAt, int subscriptionId, long sentTimestampMillis) {
- this.recipient = recipient;
- this.message = message;
- this.expiresIn = expiresIn;
- this.expireStartedAt= expireStartedAt;
- this.subscriptionId = subscriptionId;
- this.sentTimestampMillis = sentTimestampMillis;
- }
-
- public static OutgoingTextMessage from(VisibleMessage message, Address recipient, long expiresInMillis, long expireStartedAt) {
- return new OutgoingTextMessage(recipient, message.getText(), expiresInMillis, expireStartedAt, -1, message.getSentTimestamp());
- }
-
- public static OutgoingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Address recipient, Long sentTimestamp, long expiresInMillis, long expireStartedAt) {
- String url = openGroupInvitation.getUrl();
- String name = openGroupInvitation.getName();
- if (url == null || name == null) { return null; }
- // FIXME: Doing toJSON() to get the body here is weird
- String body = UpdateMessageData.Companion.buildOpenGroupInvitation(url, name).toJSON();
- OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, body, expiresInMillis, expireStartedAt, -1, sentTimestamp);
- outgoingTextMessage.isOpenGroupInvitation = true;
- return outgoingTextMessage;
- }
-
- public long getExpiresIn() {
- return expiresIn;
- }
-
- public long getExpireStartedAt() {
- return expireStartedAt;
- }
-
- public int getSubscriptionId() {
- return subscriptionId;
- }
-
- public String getMessageBody() {
- return message;
- }
-
- public Address getRecipient() {
- return recipient;
- }
-
- public long getSentTimestampMillis() {
- return sentTimestampMillis;
- }
-
- public boolean isSecureMessage() {
- return true;
- }
-
- public boolean isOpenGroupInvitation() { return isOpenGroupInvitation; }
-}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt
new file mode 100644
index 0000000000..912da3d5c7
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt
@@ -0,0 +1,53 @@
+package org.session.libsession.messaging.messages.signal
+
+import network.loki.messenger.libsession_util.protocol.ProFeature
+import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
+import org.session.libsession.messaging.messages.visible.VisibleMessage
+import org.session.libsession.messaging.utilities.UpdateMessageData
+import org.session.libsession.utilities.Address
+
+data class OutgoingTextMessage(
+ val recipient: Address,
+ val message: String?,
+ val expiresInMillis: Long,
+ val expireStartedAtMillis: Long,
+ val sentTimestampMillis: Long,
+ val isOpenGroupInvitation: Boolean,
+ val proFeatures: Set = emptySet()
+) {
+ constructor(
+ message: VisibleMessage,
+ recipient: Address,
+ expiresInMillis: Long,
+ expireStartedAtMillis: Long,
+ ): this(
+ recipient = recipient,
+ message = message.text,
+ expiresInMillis = expiresInMillis,
+ expireStartedAtMillis = expireStartedAtMillis,
+ sentTimestampMillis = message.sentTimestamp!!,
+ isOpenGroupInvitation = false,
+ )
+
+ companion object {
+ fun fromOpenGroupInvitation(
+ invitation: OpenGroupInvitation,
+ recipient: Address,
+ sentTimestampMillis: Long,
+ expiresInMillis: Long,
+ expireStartedAtMillis: Long,
+ ): OutgoingTextMessage? {
+ return OutgoingTextMessage(
+ recipient = recipient,
+ message = UpdateMessageData.buildOpenGroupInvitation(
+ url = invitation.url ?: return null,
+ name = invitation.name ?: return null,
+ ).toJSON(),
+ expiresInMillis = expiresInMillis,
+ expireStartedAtMillis = expireStartedAtMillis,
+ sentTimestampMillis = sentTimestampMillis,
+ isOpenGroupInvitation = true,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt
deleted file mode 100644
index b5677282cb..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package org.session.libsession.messaging.messages.visible
-
-import com.google.protobuf.ByteString
-import org.session.libsignal.utilities.Log
-import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile
-import org.thoughtcrime.securesms.util.DateUtils.Companion.asEpochMillis
-import org.thoughtcrime.securesms.util.DateUtils.Companion.asEpochSeconds
-import org.thoughtcrime.securesms.util.DateUtils.Companion.millsToInstant
-import org.thoughtcrime.securesms.util.DateUtils.Companion.secondsToInstant
-import org.thoughtcrime.securesms.util.DateUtils.Companion.toEpochSeconds
-import java.time.Instant
-import java.time.ZonedDateTime
-
-class Profile(
- var displayName: String? = null,
- var profileKey: ByteArray? = null,
- var profilePictureURL: String? = null,
- var profileUpdated: Instant? = null
-) {
-
- companion object {
- const val TAG = "Profile"
-
- fun fromProto(proto: SignalServiceProtos.DataMessage): Profile? {
- val profileProto = proto.profile ?: return null
- val displayName = profileProto.displayName ?: return null
- val profileKey = proto.profileKey
- val profilePictureURL = profileProto.profilePicture
- val profileUpdated = profileProto.lastProfileUpdateSeconds
- .takeIf { profileProto.hasLastProfileUpdateSeconds() }
- ?.secondsToInstant()
-
- if (profileKey != null && profilePictureURL != null) {
- return Profile(displayName, profileKey.toByteArray(), profilePictureURL, profileUpdated = profileUpdated)
- } else {
- return Profile(displayName, profileUpdated = profileUpdated)
- }
- }
- }
-
- fun toProto(): SignalServiceProtos.DataMessage? {
- val displayName = displayName
- if (displayName == null) {
- Log.w(TAG, "Couldn't construct profile proto from: $this")
- return null
- }
- val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
- val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder()
- profileProto.displayName = displayName
- profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(it) }
- profilePictureURL?.let { profileProto.profilePicture = it }
- profileUpdated?.let { profileProto.lastProfileUpdateSeconds = it.toEpochSeconds() }
- // Build
- try {
- dataMessageProto.profile = profileProto.build()
- return dataMessageProto.build()
- } catch (e: Exception) {
- Log.w(TAG, "Couldn't construct profile proto from: $this")
- return null
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt
index 42059c1c50..410d244885 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt
@@ -1,12 +1,18 @@
package org.session.libsession.messaging.messages.visible
+import androidx.annotation.Keep
import network.loki.messenger.BuildConfig
-import org.session.libsession.messaging.MessagingModuleConfiguration
+import network.loki.messenger.libsession_util.protocol.ProFeature
+import network.loki.messenger.libsession_util.protocol.ProMessageFeature
+import network.loki.messenger.libsession_util.protocol.ProProfileFeature
+import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.copyExpiration
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.pro.toProMessageBitSetValue
+import org.thoughtcrime.securesms.pro.toProProfileBitSetValue
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
/**
@@ -20,13 +26,17 @@ data class VisibleMessage(
val attachmentIDs: MutableList = mutableListOf(),
var quote: Quote? = null,
var linkPreview: LinkPreview? = null,
- var profile: Profile? = null,
var openGroupInvitation: OpenGroupInvitation? = null,
var reaction: Reaction? = null,
var hasMention: Boolean = false,
- var blocksMessageRequests: Boolean = false
+ var blocksMessageRequests: Boolean = false,
+ var proFeatures: Set = emptySet()
) : Message() {
+ // This empty constructor is needed for kryo serialization
+ @Keep
+ constructor(): this(proFeatures = emptySet())
+
override val isSelfSendValid: Boolean = true
override fun shouldDiscardIfBlocked(): Boolean = true
@@ -54,18 +64,19 @@ data class VisibleMessage(
if (it.hasQuote()) quote = Quote.fromProto(it.quote)
linkPreview = it.previewList.firstOrNull()?.let(LinkPreview::fromProto)
if (it.hasOpenGroupInvitation()) openGroupInvitation = it.openGroupInvitation?.let(OpenGroupInvitation::fromProto)
- // TODO Contact
- profile = Profile.fromProto(it)
if (it.hasReaction()) reaction = it.reaction?.let(Reaction::fromProto)
blocksMessageRequests = it.hasBlocksCommunityMessageRequests() && it.blocksCommunityMessageRequests
}.copyExpiration(proto)
}
}
- override fun toProto(): SignalServiceProtos.Content? {
- val proto = SignalServiceProtos.Content.newBuilder()
- // Profile
- val dataMessage = profile?.toProto()?.toBuilder() ?: SignalServiceProtos.DataMessage.newBuilder()
+ protected override fun buildProto(
+ builder: SignalServiceProtos.Content.Builder,
+ messageDataProvider: MessageDataProvider
+ ) {
+ val dataMessage = builder.dataMessageBuilder
+
+
// Text
if (text != null) { dataMessage.body = text }
// Quote
@@ -89,8 +100,7 @@ data class VisibleMessage(
dataMessage.openGroupInvitation = openGroupInvitationProto
}
// Attachments
- val database = MessagingModuleConfiguration.shared.messageDataProvider
- val attachments = attachmentIDs.mapNotNull { database.getSignalAttachmentPointer(it) }
+ val attachments = attachmentIDs.mapNotNull { messageDataProvider.getSignalAttachmentPointer(it) }
if (attachments.any { it.url.isNullOrEmpty() }) {
if (BuildConfig.DEBUG) {
Log.w(TAG, "Sending a message before all associated attachments have been uploaded.")
@@ -98,9 +108,6 @@ data class VisibleMessage(
}
val pointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
dataMessage.addAllAttachments(pointers)
- // TODO: Contact
- // Expiration timer on the message
- proto.applyExpiryMode()
// Community blocked message requests flag
dataMessage.blocksCommunityMessageRequests = blocksMessageRequests
@@ -108,13 +115,18 @@ data class VisibleMessage(
if (syncTarget != null) {
dataMessage.syncTarget = syncTarget
}
- // Build
- return try {
- proto.dataMessage = dataMessage.build()
- proto.build()
- } catch (e: Exception) {
- Log.w(TAG, "Couldn't construct visible message proto from: $this")
- null
+
+ // Pro features
+ if (proFeatures.any { it is ProMessageFeature }) {
+ builder.proMessageBuilder.setMsgBitset(
+ proFeatures.toProMessageBitSetValue()
+ )
+ }
+
+ if (proFeatures.any { it is ProProfileFeature }) {
+ builder.proMessageBuilder.setProfileBitset(
+ proFeatures.toProProfileBitSetValue()
+ )
}
}
// endregion
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..93fac63e91 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
@@ -62,7 +62,7 @@ data class OpenGroupMessage(
}.getOrNull() ?: return null
}
else {
- val x25519PublicKey = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair().publicKey.serialize()
+ val x25519PublicKey = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair().pubKey.data
if (sender != x25519PublicKey.toHexString() && !userEdKeyPair.pubKey.data.toHexString().equals(sender?.removingIdPrefixIfNeeded(), true)) return null
try {
ED25519.sign(
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..a16f8d24e8
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/GroupMessageHandler.kt
@@ -0,0 +1,217 @@
+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.protos.SignalServiceProtos
+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?, proto: SignalServiceProtos.Content) {
+ 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(proto)?.let { updates ->
+ profileUpdateHandler.handleProfileUpdate(
+ senderId = AccountId(message.sender!!),
+ updates = updates,
+ fromCommunity = null // Groupv2 is not a community
+ )
+ }
+
+ when {
+ inner.hasInviteMessage() -> handleNewLibSessionClosedGroupMessage(message, proto)
+ inner.hasInviteResponse() -> handleInviteResponse(message, groupId!!)
+ inner.hasPromoteMessage() -> handlePromotionMessage(message, proto)
+ 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, proto: SignalServiceProtos.Content) {
+ 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 = if (proto.hasDataMessage() && proto.dataMessage.hasProfile() && proto.dataMessage.profile.hasDisplayName())
+ proto.dataMessage.profile.displayName
+ else null,
+ 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, proto: SignalServiceProtos.Content) {
+ 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 = if (proto.hasDataMessage() && proto.dataMessage.hasProfile() && proto.dataMessage.profile.hasDisplayName())
+ proto.dataMessage.profile.displayName
+ else null,
+ 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/MessageDecrypter.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt
index 4ee0cd8bc1..af8e0c4d2c 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
@@ -2,14 +2,14 @@ package org.session.libsession.messaging.sending_receiving
import network.loki.messenger.libsession_util.SessionEncrypt
import network.loki.messenger.libsession_util.util.BlindKeyAPI
+import network.loki.messenger.libsession_util.util.KeyPair
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error
-import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.utilities.Hex
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 {
/**
@@ -20,9 +20,9 @@ object MessageDecrypter {
*
* @return the padded plaintext.
*/
- fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair {
- val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
- val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removingIdPrefixIfNeeded())
+ fun decrypt(ciphertext: ByteArray, x25519KeyPair: KeyPair): Pair {
+ val recipientX25519PrivateKey = x25519KeyPair.secretKey.data
+ val recipientX25519PublicKey = x25519KeyPair.pubKey.data.removingIdPrefixIfNeeded()
val (id, data) = SessionEncrypt.decryptIncoming(
x25519PubKey = recipientX25519PublicKey,
x25519PrivKey = recipientX25519PrivateKey,
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..d026b4fb26 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
@@ -4,10 +4,10 @@ import network.loki.messenger.libsession_util.SessionEncrypt
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsignal.utilities.Hex
-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 {
/**
@@ -19,7 +19,7 @@ object MessageEncrypter {
* @return the encrypted message.
*/
internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
- val userED25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair() ?: throw Error.NoUserED25519KeyPair
+ val userED25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair() ?: throw Error.NoUserED25519KeyPair()
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removingIdPrefixIfNeeded())
try {
@@ -30,26 +30,8 @@ object MessageEncrypter {
).data
} catch (exception: Exception) {
Log.d("Loki", "Couldn't encrypt message due to error: $exception.")
- throw Error.EncryptionFailed
+ throw Error.EncryptionFailed()
}
}
- internal fun encryptBlinded(
- plaintext: ByteArray,
- recipientBlindedId: String,
- serverPublicKey: String
- ): ByteArray {
- if (IdPrefix.fromValue(recipientBlindedId) != IdPrefix.BLINDED) throw Error.SigningFailed
- val userEdKeyPair =
- MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair() ?: throw Error.NoUserED25519KeyPair
- val recipientBlindedPublicKey = Hex.fromStringCondensed(recipientBlindedId.removingIdPrefixIfNeeded())
-
- return SessionEncrypt.encryptForBlindedRecipient(
- message = plaintext,
- myEd25519Privkey = userEdKeyPair.secretKey.data,
- serverPubKey = Hex.fromStringCondensed(serverPublicKey),
- recipientBlindId = byteArrayOf(0x15) + recipientBlindedPublicKey
- ).data
- }
-
}
\ No newline at end of file
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
new file mode 100644
index 0000000000..9caf81ae8b
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt
@@ -0,0 +1,297 @@
+package org.session.libsession.messaging.sending_receiving
+
+import dagger.Lazy
+import network.loki.messenger.libsession_util.SessionEncrypt
+import network.loki.messenger.libsession_util.protocol.DecodedEnvelope
+import network.loki.messenger.libsession_util.protocol.DecodedPro
+import network.loki.messenger.libsession_util.protocol.SessionProtocol
+import network.loki.messenger.libsession_util.util.BitSet
+import network.loki.messenger.libsession_util.util.asSequence
+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.VisibleMessage
+import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.session.libsession.snode.SnodeClock
+import org.session.libsession.utilities.Address
+import org.session.libsession.utilities.ConfigFactoryProtocol
+import org.session.libsession.utilities.TextSecurePreferences
+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.Hex
+import org.session.libsignal.utilities.IdPrefix
+import org.thoughtcrime.securesms.pro.ProStatusManager
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.math.abs
+
+@Singleton
+class MessageParser @Inject constructor(
+ private val configFactory: ConfigFactoryProtocol,
+ private val storage: StorageProtocol,
+ private val snodeClock: SnodeClock,
+ private val prefs: TextSecurePreferences,
+) {
+
+ //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(
+ decodedEnvelope: DecodedEnvelope,
+ relaxSignatureCheck: Boolean,
+ checkForBlockStatus: Boolean,
+ isForGroup: Boolean,
+ currentUserId: AccountId,
+ currentUserBlindedIDs: List,
+ senderIdPrefix: IdPrefix
+ ): Pair {
+ return parseMessage(
+ sender = AccountId(senderIdPrefix, decodedEnvelope.senderX25519PubKey.data),
+ contentPlaintext = decodedEnvelope.contentPlainText.data,
+ pro = decodedEnvelope.decodedPro,
+ messageTimestampMs = decodedEnvelope.timestamp.toEpochMilli(),
+ relaxSignatureCheck = relaxSignatureCheck,
+ checkForBlockStatus = checkForBlockStatus,
+ isForGroup = isForGroup,
+ currentUserId = currentUserId,
+ currentUserBlindedIDs = currentUserBlindedIDs,
+ )
+ }
+
+ private fun parseMessage(
+ sender: AccountId,
+ contentPlaintext: ByteArray,
+ pro: DecodedPro?,
+ messageTimestampMs: Long,
+ relaxSignatureCheck: Boolean,
+ checkForBlockStatus: Boolean,
+ isForGroup: Boolean,
+ currentUserId: AccountId,
+ currentUserBlindedIDs: 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 currentUserBlindedIDs
+ 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
+
+ // Only process pro features post pro launch
+ if (prefs.forcePostPro()) {
+ (message as? VisibleMessage)?.proFeatures = buildSet {
+ pro?.proMessageFeatures?.asSequence()?.let(::addAll)
+ pro?.proProfileFeatures?.asSequence()?.let(::addAll)
+ }
+ }
+
+ // 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: 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")
+ }
+ storage.addReceivedMessageTimestamp(messageTimestampMs)
+
+ return message to proto
+ }
+
+
+ fun parse1o1Message(
+ data: ByteArray,
+ serverHash: String?,
+ currentUserEd25519PrivKey: ByteArray,
+ currentUserId: AccountId,
+ ): Pair {
+ val envelop = SessionProtocol.decodeFor1o1(
+ myEd25519PrivKey = currentUserEd25519PrivKey,
+ payload = data,
+ nowEpochMs = snodeClock.currentTimeMills(),
+ proBackendPubKey = proBackendKey,
+ )
+
+ return parseMessage(
+ decodedEnvelope = envelop,
+ relaxSignatureCheck = false,
+ checkForBlockStatus = true,
+ isForGroup = false,
+ senderIdPrefix = IdPrefix.STANDARD,
+ currentUserId = currentUserId,
+ currentUserBlindedIDs = emptyList(),
+ ).also { (message, _) ->
+ message.serverHash = serverHash
+ }
+ }
+
+ fun parseGroupMessage(
+ data: ByteArray,
+ serverHash: String,
+ groupId: AccountId,
+ currentUserEd25519PrivKey: ByteArray,
+ currentUserId: AccountId,
+ ): Pair {
+ val keys = configFactory.withGroupConfigs(groupId) {
+ it.groupKeys.groupKeys()
+ }
+
+ val decoded = SessionProtocol.decodeForGroup(
+ payload = data,
+ myEd25519PrivKey = currentUserEd25519PrivKey,
+ nowEpochMs = snodeClock.currentTimeMills(),
+ groupEd25519PublicKey = groupId.pubKeyBytes,
+ groupEd25519PrivateKeys = keys.toTypedArray(),
+ proBackendPubKey = proBackendKey
+ )
+
+ return parseMessage(
+ decodedEnvelope = decoded,
+ relaxSignatureCheck = false,
+ checkForBlockStatus = false,
+ isForGroup = true,
+ senderIdPrefix = IdPrefix.STANDARD,
+ currentUserId = currentUserId,
+ currentUserBlindedIDs = emptyList(),
+ ).also { (message, _) ->
+ message.serverHash = serverHash
+ }
+ }
+
+ fun parseCommunityMessage(
+ msg: OpenGroupApi.Message,
+ currentUserId: AccountId,
+ currentUserBlindedIDs: List,
+ ): Pair? {
+ if (msg.data.isNullOrBlank()) {
+ return null
+ }
+
+ val decoded = SessionProtocol.decodeForCommunity(
+ payload = Base64.decode(msg.data),
+ nowEpochMs = snodeClock.currentTimeMills(),
+ proBackendPubKey = proBackendKey,
+ )
+
+ val sender = AccountId(msg.sessionId)
+
+ return parseMessage(
+ contentPlaintext = decoded.contentPlainText.data,
+ pro = decoded.decodedPro,
+ relaxSignatureCheck = true,
+ checkForBlockStatus = false,
+ isForGroup = false,
+ currentUserId = currentUserId,
+ sender = sender,
+ messageTimestampMs = (msg.posted * 1000).toLong(),
+ currentUserBlindedIDs = currentUserBlindedIDs,
+ ).also { (message, _) ->
+ message.openGroupServerMessageID = msg.id
+ }
+ }
+
+ fun parseCommunityDirectMessage(
+ msg: OpenGroupApi.DirectMessage,
+ communityServerPubKeyHex: String,
+ currentUserEd25519PrivKey: ByteArray,
+ currentUserId: AccountId,
+ currentUserBlindedIDs: List,
+ ): Pair {
+ val (senderId, plaintext) = SessionEncrypt.decryptForBlindedRecipient(
+ ciphertext = Base64.decode(msg.message),
+ myEd25519Privkey = currentUserEd25519PrivKey,
+ openGroupPubkey = Hex.fromStringCondensed(communityServerPubKeyHex),
+ senderBlindedId = Hex.fromStringCondensed(msg.sender),
+ recipientBlindId = Hex.fromStringCondensed(msg.recipient),
+ )
+
+ val decoded = SessionProtocol.decodeForCommunity(
+ payload = plaintext.data,
+ nowEpochMs = snodeClock.currentTimeMills(),
+ proBackendPubKey = proBackendKey,
+ )
+
+ val sender = Address.Standard(AccountId(senderId))
+
+ return parseMessage(
+ contentPlaintext = decoded.contentPlainText.data,
+ pro = decoded.decodedPro,
+ relaxSignatureCheck = true,
+ checkForBlockStatus = false,
+ isForGroup = false,
+ currentUserId = currentUserId,
+ sender = sender.accountId,
+ messageTimestampMs = (msg.postedAt * 1000),
+ currentUserBlindedIDs = currentUserBlindedIDs,
+ )
+ }
+}
\ 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 9e05ecce3e..e2b42f3976 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,8 @@
package org.session.libsession.messaging.sending_receiving
import network.loki.messenger.libsession_util.util.BlindKeyAPI
-import org.session.libsession.messaging.MessagingModuleConfiguration
+import network.loki.messenger.libsession_util.util.KeyPair
+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
@@ -21,9 +22,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 {
+@Deprecated("This class only exists so the old BatchMessageReceiver can function. New code should use MessageHandler directly.")
+@Singleton
+class MessageReceiver @Inject constructor(
+ private val storage: StorageProtocol,
+) {
internal sealed class Error(message: String) : Exception(message) {
object DuplicateMessage: Error("Duplicate message.")
@@ -60,7 +67,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 +97,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 +111,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
}
@@ -117,7 +123,11 @@ object MessageReceiver {
var encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex)
fun decrypt() {
try {
- val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), encryptionKeyPair)
+ val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(),
+ KeyPair(
+ pubKey = encryptionKeyPair.publicKey.serialize(),
+ secretKey = encryptionKeyPair.privateKey.serialize()
+ ))
plaintext = decryptionResult.first
sender = decryptionResult.second
} catch (e: Exception) {
@@ -172,7 +182,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/MessageRequestResponseHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt
index 0d43629fc9..a1db7543b2 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
@@ -1,8 +1,8 @@
package org.session.libsession.messaging.sending_receiving
+import network.loki.messenger.libsession_util.util.BitSet
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.ProfileUpdateHandler
-import org.session.libsession.messaging.messages.ProfileUpdateHandler.Updates.Companion.toUpdates
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
import org.session.libsession.messaging.messages.visible.VisibleMessage
@@ -12,8 +12,8 @@ import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.recipients.Recipient
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.BlindMappingRepository
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.RecipientRepository
@@ -33,12 +33,16 @@ class MessageRequestResponseHandler @Inject constructor(
private val blindMappingRepository: BlindMappingRepository,
) {
- suspend 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,15 @@ class MessageRequestResponseHandler @Inject constructor(
}
}
- suspend fun handleExplicitRequestResponseMessage(message: MessageRequestResponse) {
+ fun handleExplicitRequestResponseMessage(
+ ctx: ReceivedMessageProcessor.MessageProcessingContext?,
+ message: MessageRequestResponse,
+ proto: SignalServiceProtos.Content,
+ ) {
val (sender, receiver) = fetchSenderAndReceiver(message) ?: return
// Always handle explicit request response
handleRequestResponse(
+ ctx = ctx,
messageSender = sender,
messageReceiver = receiver,
messageTimestampMs = message.sentTimestamp!!,
@@ -72,7 +82,7 @@ class MessageRequestResponseHandler @Inject constructor(
// Always process the profile update if any. We don't need
// to process profile for other kind of messages as they should be handled elsewhere
- message.profile?.toUpdates()?.let { updates ->
+ ProfileUpdateHandler.Updates.create(proto)?.let { updates ->
profileUpdateHandler.get().handleProfileUpdate(
senderId = (sender.address as Address.Standard).accountId,
updates = updates,
@@ -81,8 +91,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 +102,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()
@@ -101,6 +111,7 @@ class MessageRequestResponseHandler @Inject constructor(
}
private fun handleRequestResponse(
+ ctx: ReceivedMessageProcessor.MessageProcessingContext?,
messageSender: Recipient,
messageReceiver: Recipient,
messageTimestampMs: Long,
@@ -141,21 +152,20 @@ class MessageRequestResponseHandler @Inject constructor(
if (!didApproveMe) {
mmsDatabase.insertSecureDecryptedMessageInbox(
retrieved = IncomingMediaMessage(
- messageSender.address,
- messageTimestampMs,
- -1,
- 0L,
- 0L,
- true,
- false,
- Optional.absent(),
- Optional.absent(),
- Optional.absent(),
- null,
- Optional.absent(),
- Optional.absent(),
- Optional.absent(),
- Optional.absent()
+ from = messageSender.address,
+ sentTimeMillis = messageTimestampMs,
+ expiresIn = 0L,
+ expireStartedAt = 0L,
+ isMessageRequestResponse = true,
+ hasMention = false,
+ body = null,
+ group = null,
+ attachments = emptyList(),
+ proFeatures = emptySet(),
+ messageContent = null,
+ quote = null,
+ linkPreviews = emptyList(),
+ dataExtractionNotification = null
),
threadId,
runThreadUpdate = true,
@@ -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/MessageSender.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
index 0b5158ce5f..590dec0032 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,18 +1,22 @@
package org.session.libsession.messaging.sending_receiving
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.async
+import com.google.protobuf.ByteString
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.launch
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.PRIORITY_HIDDEN
+import network.loki.messenger.libsession_util.PRIORITY_VISIBLE
import network.loki.messenger.libsession_util.Namespace
+import network.loki.messenger.libsession_util.ReadableUserProfile
+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.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
@@ -26,66 +30,129 @@ 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.SnodeClock
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.utilities.Address
-import org.session.libsession.utilities.SSKEnvironment
-import org.session.libsignal.crypto.PushTransportDetails
+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.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.dependencies.ManagerScope
+import org.thoughtcrime.securesms.pro.copyFromLibSession
+import org.thoughtcrime.securesms.pro.db.ProDatabase
+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
-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,
+ private val proDatabase: ProDatabase,
+ private val snodeClock: SnodeClock,
+ @param:ManagerScope private val scope: CoroutineScope,
+) {
// Error
- sealed class Error(val description: String) : Exception(description) {
- object InvalidMessage : Error("Invalid message.")
- object ProtoConversionFailed : Error("Couldn't convert message to proto.")
- object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
- object SigningFailed : Error("Couldn't sign message.")
- object EncryptionFailed : Error("Couldn't encrypt message.")
- data class InvalidDestination(val destination: Destination): Error("Can't send this way to $destination")
+ sealed class Error(val description: String, cause: Throwable? = null) : Exception(description, cause) {
+ class InvalidMessage : Error("Invalid message.")
+ class ProtoConversionFailed(cause: Throwable) : Error("Couldn't convert message to proto.", cause)
+ class NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
+ class SigningFailed : Error("Couldn't sign message.")
+ class EncryptionFailed : Error("Couldn't encrypt message.")
// Closed groups
- object NoThread : Error("Couldn't find a thread associated with the given group public key.")
- object NoKeyPair: Error("Couldn't find a private key associated with the given group public key.")
- object InvalidClosedGroupUpdate : Error("Invalid group update.")
+ class InvalidClosedGroupUpdate : Error("Invalid group update.")
internal val isRetryable: Boolean = when (this) {
- is InvalidMessage, ProtoConversionFailed, InvalidClosedGroupUpdate -> false
+ is InvalidMessage, is ProtoConversionFailed, is InvalidClosedGroupUpdate -> false
else -> true
}
}
+
+ private fun SignalServiceProtos.DataMessage.Builder.copyProfileFromConfig() {
+ configFactory.withUserConfigs {
+ val pic = it.userProfile.getPic()
+
+ profileBuilder.setDisplayName(it.userProfile.getName().orEmpty())
+ .setProfilePicture(pic.url)
+ .setLastProfileUpdateSeconds(it.userProfile.getProfileUpdatedSeconds())
+
+ setProfileKey(ByteString.copyFrom(pic.keyAsByteArray))
+ }
+ }
+
+ private fun SignalServiceProtos.MessageRequestResponse.Builder.copyProfileFromConfig() {
+ configFactory.withUserConfigs {
+ val pic = it.userProfile.getPic()
+
+ profileBuilder.setDisplayName(it.userProfile.getName().orEmpty())
+ .setProfilePicture(pic.url)
+ .setLastProfileUpdateSeconds(it.userProfile.getProfileUpdatedSeconds())
+
+ setProfileKey(ByteString.copyFrom(pic.keyAsByteArray))
+ }
+ }
+
// 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)
}
}
+ private fun buildProto(msg: Message): SignalServiceProtos.Content {
+ try {
+ val builder = SignalServiceProtos.Content.newBuilder()
+
+ msg.toProto(builder, messageDataProvider)
+
+ // Attach pro proof
+ configFactory.withUserConfigs { it.userProfile.getProConfig() }?.proProof?.let { proof ->
+ builder.proMessageBuilder.proofBuilder.copyFromLibSession(proof)
+ }
+
+ // Attach the user's profile if needed
+ when {
+ builder.hasDataMessage() && !builder.dataMessageBuilder.hasProfile() -> {
+ builder.dataMessageBuilder.copyProfileFromConfig()
+ }
+
+ builder.hasMessageRequestResponse() && !builder.messageRequestResponseBuilder.hasProfile() -> {
+ builder.messageRequestResponseBuilder.copyProfileFromConfig()
+ }
+ }
+
+ return builder.build()
+ } catch (e: Exception) {
+ throw Error.ProtoConversionFailed(e)
+ }
+ }
+
// 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()
+ val userEd25519PrivKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.data) {
+ "Missing user key"
+ }
// Set the timestamp, sender and recipient
- val messageSendTime = nowWithOffset
+ val messageSendTime = snodeClock.currentTimeMills()
if (message.sentTimestamp == null) {
message.sentTimestamp =
messageSendTime // Visible messages will already have their sent timestamp set
@@ -95,15 +162,15 @@ object MessageSender {
// 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)
// Validate the message
if (!message.isValid()) {
- throw Error.InvalidMessage
+ throw Error.InvalidMessage()
}
// Stop here if this is a self-send, unless it's:
// • a configuration message
@@ -113,79 +180,41 @@ object MessageSender {
&& !isSyncMessage
&& message !is UnsendRequest
) {
- throw Error.InvalidMessage
- }
- // Attach the user's profile if needed
- if (message is VisibleMessage) {
- message.profile = storage.getUserProfile()
+ throw Error.InvalidMessage()
}
- if (message is MessageRequestResponse) {
- message.profile = storage.getUserProfile()
- }
- // Convert it to protobuf
- val proto = message.toProto()?.toBuilder() ?: throw Error.ProtoConversionFailed
- if (message is GroupUpdated) {
- if (message.profile != null) {
- proto.mergeDataMessage(message.profile.toProto())
- }
- }
-
- // 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
+ SessionProtocol.encodeFor1o1(
+ plaintext = buildProto(message).toByteArray(),
+ myEd25519PrivKey = userEd25519PrivKey,
+ timestampMs = message.sentTimestamp!!,
+ recipientPubKey = Hex.fromStringCondensed(destination.publicKey),
+ proRotatingEd25519PrivKey = null,
+ )
}
- is Destination.ClosedGroup -> {
- kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
- senderPublicKey = destination.publicKey
- }
- 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 = buildProto(message).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
)
@@ -193,8 +222,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)
@@ -202,57 +229,39 @@ object MessageSender {
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
}
}
@@ -275,7 +284,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,10 +294,8 @@ 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
+ message.sentTimestamp = snodeClock.currentTimeMills()
}
// Attach the blocks message requests info
configFactory.withUserConfigs { configs ->
@@ -296,10 +303,10 @@ object MessageSender {
message.blocksMessageRequests = !configs.userProfile.getCommunityMessageRequests()
}
}
- val userEdKeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair()!!
- var serverCapabilities = listOf()
+ val userEdKeyPair = storage.getUserED25519KeyPair()!!
+ var serverCapabilities: List
var blindedPublicKey: ByteArray? = null
- when(destination) {
+ when (destination) {
is Destination.OpenGroup -> {
serverCapabilities = storage.getServerCapabilities(destination.server).orEmpty()
storage.getOpenGroupPublicKey(destination.server)?.let {
@@ -316,16 +323,9 @@ object MessageSender {
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
@@ -335,24 +335,21 @@ object MessageSender {
message.sender = messageSender
try {
- // Attach the user's profile if needed
- if (message is VisibleMessage) {
- message.profile = storage.getUserProfile()
- }
- val content = message.toProto()!!.toBuilder()
- .setSigTimestampMs(message.sentTimestamp!!)
- .build()
+ val content = buildProto(message)
when (destination) {
is Destination.OpenGroup -> {
- val whisperMods = if (destination.whisperTo.isNullOrEmpty() && destination.whisperMods) "mods" else null
+ val whisperMods = if (destination.whisperTo.isEmpty() && destination.whisperMods) "mods" else null
message.recipient = "${destination.server}.${destination.roomToken}.${destination.whisperTo}.$whisperMods"
// Validate the message
if (message !is VisibleMessage || !message.isValid()) {
- throw Error.InvalidMessage
+ 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!!,
@@ -376,15 +373,17 @@ object MessageSender {
message.recipient = destination.blindedPublicKey
// Validate the message
if (message !is VisibleMessage || !message.isValid()) {
- throw Error.InvalidMessage
+ 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,
@@ -405,8 +404,7 @@ object MessageSender {
}
// 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!!)
@@ -427,21 +425,9 @@ object MessageSender {
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(
@@ -459,7 +445,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)
}
@@ -472,7 +458,7 @@ object MessageSender {
if (message is ExpirationTimerUpdate) message.syncTarget = destination.publicKey
message.id?.let(storage::markAsSyncing)
- GlobalScope.launch {
+ scope.launch {
try {
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true)
} catch (ec: Exception) {
@@ -483,12 +469,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
}
@@ -497,9 +481,7 @@ 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))
@@ -517,24 +499,23 @@ object MessageSender {
send(message, address)
}
- @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 +528,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/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt
index 277efe3054..979e83c8cb 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
@@ -10,12 +10,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import network.loki.messenger.R
-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.PRIORITY_HIDDEN
+import network.loki.messenger.libsession_util.PRIORITY_VISIBLE
import network.loki.messenger.libsession_util.ED25519
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.BlindKeyAPI
import network.loki.messenger.libsession_util.util.ExpiryMode
+import network.loki.messenger.libsession_util.util.Util
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol
import org.session.libsession.database.userAuth
@@ -53,6 +54,7 @@ import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID
import org.session.libsession.utilities.SSKEnvironment
+import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.MessageType
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.RecipientData
@@ -83,6 +85,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,
@@ -100,6 +103,7 @@ class ReceivedMessageHandler @Inject constructor(
@param:ManagerScope private val scope: CoroutineScope,
private val configFactory: ConfigFactoryProtocol,
private val messageRequestResponseHandler: Provider,
+ private val prefs: TextSecurePreferences,
) {
suspend fun handle(
@@ -114,7 +118,11 @@ class ReceivedMessageHandler @Inject constructor(
when (message) {
is ReadReceipt -> handleReadReceipt(message)
is TypingIndicator -> handleTypingIndicator(message)
- is GroupUpdated -> handleGroupUpdated(message, (threadAddress as? Address.Group)?.accountId)
+ is GroupUpdated -> handleGroupUpdated(
+ message = message,
+ closedGroup = (threadAddress as? Address.Group)?.accountId,
+ proto = proto
+ )
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.
@@ -129,7 +137,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, proto)
is VisibleMessage -> handleVisibleMessage(
message = message,
proto = proto,
@@ -183,6 +191,9 @@ class ReceivedMessageHandler @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
+
val address = Address.fromSerialized(senderPublicKey)
val threadID = storage.getThreadId(address) ?: return
typingIndicators.didReceiveTypingStartedMessage(threadID, address, 1)
@@ -213,7 +224,6 @@ class ReceivedMessageHandler @Inject constructor(
val senderPublicKey = message.sender!!
val notification: DataExtractionNotificationInfoMessage = when(message.kind) {
- is DataExtractionNotification.Kind.Screenshot -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.SCREENSHOT)
is DataExtractionNotification.Kind.MediaSaved -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED)
else -> return
}
@@ -296,7 +306,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
@@ -385,7 +395,7 @@ class ReceivedMessageHandler @Inject constructor(
// 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
+ val messageText = message.text?.let { Util.truncateCodepoints(it, maxChars) } // truncate to max char limit for this message
message.text = messageText
message.hasMention = listOfNotNull(userPublicKey, context.userBlindedKey)
.any { key ->
@@ -438,14 +448,7 @@ class ReceivedMessageHandler @Inject constructor(
// - 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,
- )
+ val updates = ProfileUpdateHandler.Updates.create(proto)
if (updates != null) {
profileUpdateHandler.get().handleProfileUpdate(
@@ -484,7 +487,7 @@ class ReceivedMessageHandler @Inject constructor(
return null
}
- private fun handleGroupUpdated(message: GroupUpdated, closedGroup: AccountId?) {
+ private fun handleGroupUpdated(message: GroupUpdated, closedGroup: AccountId?, proto: SignalServiceProtos.Content) {
val inner = message.inner
if (closedGroup == null &&
!inner.hasInviteMessage() && !inner.hasPromoteMessage()) {
@@ -492,14 +495,7 @@ class ReceivedMessageHandler @Inject constructor(
}
// 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.Updates.create(proto)?.let { updates ->
profileUpdateHandler.get().handleProfileUpdate(
senderId = AccountId(message.sender!!),
updates = updates,
@@ -508,9 +504,9 @@ class ReceivedMessageHandler @Inject constructor(
}
when {
- inner.hasInviteMessage() -> handleNewLibSessionClosedGroupMessage(message)
+ inner.hasInviteMessage() -> handleNewLibSessionClosedGroupMessage(message, proto)
inner.hasInviteResponse() -> handleInviteResponse(message, closedGroup!!)
- inner.hasPromoteMessage() -> handlePromotionMessage(message)
+ inner.hasPromoteMessage() -> handlePromotionMessage(message, proto)
inner.hasInfoChangeMessage() -> handleGroupInfoChange(message, closedGroup!!)
inner.hasMemberChangeMessage() -> handleMemberChange(message, closedGroup!!)
inner.hasMemberLeftMessage() -> handleMemberLeft(message, closedGroup!!)
@@ -589,7 +585,7 @@ class ReceivedMessageHandler @Inject constructor(
groupManagerV2.handleGroupInfoChange(message, closedGroup)
}
- private fun handlePromotionMessage(message: GroupUpdated) {
+ private fun handlePromotionMessage(message: GroupUpdated, proto: SignalServiceProtos.Content) {
val promotion = message.inner.promoteMessage
val seed = promotion.groupIdentitySeed.toByteArray()
val sender = message.sender!!
@@ -602,7 +598,9 @@ class ReceivedMessageHandler @Inject constructor(
groupName = promotion.name,
adminKeySeed = seed,
promoter = adminId,
- promoterName = message.profile?.displayName,
+ promoterName = if (proto.hasDataMessage() && proto.dataMessage.hasProfile() && proto.dataMessage.profile.hasDisplayName())
+ proto.dataMessage.profile.displayName
+ else null,
promoteMessageHash = message.serverHash!!,
promoteMessageTimestamp = message.sentTimestamp!!,
)
@@ -625,7 +623,7 @@ class ReceivedMessageHandler @Inject constructor(
}
}
- private fun handleNewLibSessionClosedGroupMessage(message: GroupUpdated) {
+ private fun handleNewLibSessionClosedGroupMessage(message: GroupUpdated, proto: SignalServiceProtos.Content) {
val storage = storage
val ourUserId = storage.getUserPublicKey()!!
val invite = message.inner.inviteMessage
@@ -646,7 +644,9 @@ class ReceivedMessageHandler @Inject constructor(
groupName = invite.name,
authData = invite.memberAuthData.toByteArray(),
inviter = adminId,
- inviterName = message.profile?.displayName,
+ inviterName = if (proto.hasDataMessage() && proto.dataMessage.hasProfile() && proto.dataMessage.profile.hasDisplayName())
+ proto.dataMessage.profile.displayName
+ else null,
inviteMessageHash = message.serverHash!!,
inviteMessageTimestamp = message.sentTimestamp!!,
)
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..c4137655d1
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt
@@ -0,0 +1,612 @@
+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.PRIORITY_HIDDEN
+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
+import org.session.libsession.messaging.messages.Message
+import org.session.libsession.messaging.messages.Message.Companion.senderOrSync
+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.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
+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
+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.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
+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
+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 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 blindMappingRepository: BlindMappingRepository,
+ private val messageParser: MessageParser,
+) {
+ private val threadMutexes = ConcurrentHashMap()
+
+ private inline fun withThreadLock(
+ threadAddress: Address.Conversable,
+ block: () -> T
+ ) {
+ threadMutexes.getOrPut(threadAddress) { ReentrantLock() }.withLock {
+ block()
+ }
+ }
+
+
+ /**
+ * 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(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
+ )
+ }
+
+ 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")
+ }
+ }
+
+ fun processSwarmMessage(
+ context: MessageProcessingContext,
+ threadAddress: Address.Conversable,
+ message: Message,
+ proto: SignalServiceProtos.Content,
+ ) = withThreadLock(threadAddress) {
+ // 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@withThreadLock
+ }
+
+ // 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@withThreadLock
+ } 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,
+ proto = proto
+ )
+
+ 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(context, message, proto)
+
+ is VisibleMessage -> {
+ if (message.isSenderSelf &&
+ message.sentTimestamp != null &&
+ message.sentTimestamp!! > context.maxOutgoingMessageTimestamp
+ ) {
+ context.maxOutgoingMessageTimestamp = message.sentTimestamp!!
+ }
+
+ visibleMessageHandler.get().handleVisibleMessage(
+ ctx = context,
+ message = message,
+ threadId = threadId,
+ threadAddress = threadAddress,
+ proto = proto,
+ runThreadUpdate = false,
+ runProfileUpdate = true,
+ )
+ }
+
+ is CallMessage -> handleCallMessage(message)
+ }
+
+ }
+
+ fun processCommunityInboxMessage(
+ context: MessageProcessingContext,
+ communityServerUrl: String,
+ communityServerPubKeyHex: String,
+ message: OpenGroupApi.DirectMessage
+ ) {
+ val (message, proto) = messageParser.parseCommunityDirectMessage(
+ msg = message,
+ currentUserId = context.currentUserId,
+ currentUserEd25519PrivKey = context.currentUserEd25519KeyPair.secretKey.data,
+ currentUserBlindedIDs = context.getCurrentUserBlindedIDsByServer(communityServerUrl),
+ communityServerPubKeyHex = communityServerPubKeyHex,
+ )
+
+ val threadAddress = message.senderOrSync.toAddress() as Address.Conversable
+
+ withThreadLock(threadAddress) {
+ processSwarmMessage(
+ context = context,
+ threadAddress = threadAddress,
+ message = message,
+ proto = proto
+ )
+ }
+ }
+
+ fun processCommunityOutboxMessage(
+ context: MessageProcessingContext,
+ communityServerUrl: String,
+ communityServerPubKeyHex: String,
+ msg: OpenGroupApi.DirectMessage
+ ) {
+ val (message, proto) = messageParser.parseCommunityDirectMessage(
+ msg = msg,
+ currentUserId = context.currentUserId,
+ currentUserEd25519PrivKey = context.currentUserEd25519KeyPair.secretKey.data,
+ currentUserBlindedIDs = context.getCurrentUserBlindedIDsByServer(communityServerUrl),
+ communityServerPubKeyHex = communityServerPubKeyHex,
+ )
+
+ val threadAddress = Address.CommunityBlindedId(
+ serverUrl = communityServerUrl,
+ blindedId = Address.Blinded(AccountId(msg.recipient))
+ )
+
+ withThreadLock(threadAddress) {
+ processSwarmMessage(
+ context = context,
+ threadAddress = threadAddress,
+ message = message,
+ proto = proto
+ )
+ }
+ }
+
+ fun processCommunityMessage(
+ context: MessageProcessingContext,
+ threadAddress: Address.Community,
+ message: OpenGroupApi.Message,
+ ) = withThreadLock(threadAddress) {
+ var messageId = messageParser.parseCommunityMessage(
+ msg = message,
+ currentUserId = context.currentUserId,
+ currentUserBlindedIDs = context.getCurrentUserBlindedIDsByThread(threadAddress)
+ )?.let { (msg, proto) ->
+ processSwarmMessage(
+ 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()
+ val reactions = mutableListOf()
+
+ 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) {
+ reactions += 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) {
+ reactions += ReactionRecord(
+ messageId = messageId,
+ author = reactor,
+ emoji = emoji,
+ serverId = messageServerId,
+ count = reaction.count,
+ sortId = reaction.index,
+ )
+ }
+ }
+
+ context.setCommunityMessageReactions(messageId, reactions)
+ }
+
+ 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 == PRIORITY_HIDDEN
+ }
+
+ return hidden &&
+ // the message's sentTimestamp is earlier than the sentTimestamp of the last config
+ 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 {
+ private var recipients: HashMap? = null
+ val threadIDs: HashMap = hashMapOf()
+ private var currentUserBlindedKeysByCommunityServer: HashMap>? = null
+ val currentUserId: AccountId = AccountId(requireNotNull(storage.getUserPublicKey()) {
+ "No current user available"
+ })
+
+ 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 var blindIDMappingCache: HashMap>>? =
+ null
+
+
+ var pendingCommunityReactions: HashMap>? = null
+ private set
+
+
+ fun getBlindIDMapping(address: Address.Standard): List> {
+ val cache = blindIDMappingCache
+ ?: hashMapOf>>().also {
+ blindIDMappingCache = it
+ }
+
+ return cache.getOrPut(address) {
+ blindMappingRepository.calculateReverseMappings(address)
+ }
+ }
+
+
+ fun getThreadRecipient(threadAddress: Address.Conversable): Recipient {
+ val cache = recipients ?: hashMapOf().also {
+ recipients = it
+ }
+
+ return cache.getOrPut(threadAddress) {
+ recipientRepository.getRecipientSync(threadAddress)
+ }
+ }
+
+ fun getCurrentUserBlindedIDsByServer(serverUrl: String): List {
+ val serverPubKey = requireNotNull(storage.getOpenGroupPublicKey(serverUrl)) {
+ "No open group public key found"
+ }
+
+ val cache =
+ currentUserBlindedKeysByCommunityServer ?: hashMapOf>().also {
+ currentUserBlindedKeysByCommunityServer = it
+ }
+
+ return cache.getOrPut(serverUrl) {
+ BlindKeyAPI.blind15Ids(
+ sessionId = currentUserPublicKey,
+ serverPubKey = serverPubKey
+ ).map(::AccountId) + AccountId(
+ BlindKeyAPI.blind25Id(
+ sessionId = currentUserPublicKey,
+ serverPubKey = serverPubKey
+ )
+ )
+ }
+ }
+
+
+ fun getCurrentUserBlindedIDsByThread(address: Address.Conversable): List {
+ if (address !is Address.Community) return emptyList()
+ return getCurrentUserBlindedIDsByServer(address.serverUrl)
+ }
+
+
+ fun setCommunityMessageReactions(messageId: MessageId, reactions: List) {
+ val reactionsMap = pendingCommunityReactions
+ ?: hashMapOf>().also {
+ pendingCommunityReactions = it
+ }
+
+ reactionsMap[messageId] = reactions
+ }
+ }
+
+ 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..57ea0d6182
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt
@@ -0,0 +1,255 @@
+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.PRIORITY_HIDDEN
+import network.loki.messenger.libsession_util.PRIORITY_VISIBLE
+import network.loki.messenger.libsession_util.util.BaseCommunityInfo
+import network.loki.messenger.libsession_util.util.ExpiryMode
+import network.loki.messenger.libsession_util.util.Util
+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(
+ ctx: ReceivedMessageProcessor.MessageProcessingContext,
+ message: VisibleMessage,
+ threadId: Long,
+ threadAddress: Address.Conversable,
+ proto: SignalServiceProtos.Content,
+ runThreadUpdate: Boolean,
+ runProfileUpdate: Boolean,
+ ): MessageId? {
+ val senderAddress = message.sender!!.toAddress()
+
+ messageRequestResponseHandler.handleVisibleMessage(ctx, 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
+
+ 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)
+ 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?.let { Util.truncateCodepoints(it, maxChars) } // truncate to max char limit for this message
+ message.text = messageText
+ message.hasMention = (sequenceOf(ctx.currentUserPublicKey) + ctx.getCurrentUserBlindedIDsByThread(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,
+ // and this should only be done only for 1:1 messages
+ if (senderAddress is Address.Standard &&
+ senderAddress.address != ctx.currentUserPublicKey &&
+ threadAddress is Address.Standard) {
+ 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(proto)
+
+ 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 == ctx.currentUserPublicKey)) {
+ 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/notifications/PushRegistryV1.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt
index 827731450d..4a0509049f 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt
@@ -13,17 +13,13 @@ import org.session.libsession.snode.OnionResponse
import org.session.libsession.snode.Version
import org.session.libsession.snode.utilities.asyncPromise
import org.session.libsession.snode.utilities.await
-import org.session.libsession.utilities.Device
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.JsonUtil
-import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.emptyPromise
import org.session.libsignal.utilities.retryWithUniformInterval
@SuppressLint("StaticFieldLeak")
object PushRegistryV1 {
- private val TAG = PushRegistryV1::class.java.name
-
val context = MessagingModuleConfiguration.shared.context
private const val MAX_RETRY_COUNT = 4
@@ -32,55 +28,6 @@ object PushRegistryV1 {
@Suppress("OPT_IN_USAGE")
private val scope: CoroutineScope = GlobalScope
- fun register(
- device: Device,
- isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
- publicKey: String? = TextSecurePreferences.getLocalNumber(context),
- legacyGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllLegacyGroupPublicKeys()
- ): Promise<*, Exception> = scope.asyncPromise {
- if (isPushEnabled) {
- retryWithUniformInterval(maxRetryCount = MAX_RETRY_COUNT) { doRegister(publicKey, device, legacyGroupPublicKeys) }
- }
- }
-
- private suspend fun doRegister(publicKey: String?, device: Device, legacyGroupPublicKeys: Collection) {
- Log.d(TAG, "doRegister() called")
-
- val token = MessagingModuleConfiguration.shared.tokenFetcher.fetch()
- publicKey ?: return
-
- val parameters = mapOf(
- "token" to token,
- "pubKey" to publicKey,
- "device" to device.value,
- "legacyGroupPublicKeys" to legacyGroupPublicKeys
- )
-
- val url = "${server.url}/register_legacy_groups_only"
- val body = JsonUtil.toJson(parameters).toRequestBody("application/json".toMediaType())
- val request = Request.Builder().url(url).post(body).build()
-
- sendOnionRequest(request).await().checkError()
- Log.d(TAG, "registerV1 success")
- }
-
- /**
- * Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager.
- */
- fun unregister(): Promise<*, Exception> = scope.asyncPromise {
- Log.d(TAG, "unregisterV1 requested")
-
- retryWithUniformInterval(maxRetryCount = MAX_RETRY_COUNT) {
- val token = MessagingModuleConfiguration.shared.tokenFetcher.fetch()
- val parameters = mapOf("token" to token)
- val url = "${server.url}/unregister"
- val body = JsonUtil.toJson(parameters).toRequestBody("application/json".toMediaType())
- val request = Request.Builder().url(url).post(body).build()
- sendOnionRequest(request).await().checkError()
- Log.d(TAG, "unregisterV1 success")
- }
- }
-
// Legacy Closed Groups
fun subscribeGroup(
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..e83caa4d04 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,10 @@ 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
@@ -33,26 +28,19 @@ 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.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.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.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
-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>>
@@ -67,14 +55,12 @@ private typealias PollRequestToken = Channel>>
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 receivedMessageProcessor: ReceivedMessageProcessor,
+ private val messageParser: MessageParser,
@Assisted private val server: String,
@Assisted private val scope: CoroutineScope,
@Assisted private val pollerSemaphore: Semaphore,
@@ -168,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 }
@@ -179,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) */}
}
@@ -228,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(
@@ -237,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>(){}
)
}
)
@@ -294,136 +278,120 @@ class OpenGroupPoller @AssistedInject constructor(
private fun handleMessages(
- server: String,
roomToken: String,
messages: List
) {
- val sortedMessages = messages.sortedBy { it.seqno }
- sortedMessages.maxOfOrNull { it.seqno }?.let { seqNo ->
- storage.setLastMessageServerID(roomToken, server, seqNo)
+ val (deletions, additions) = messages.partition { it.deleted }
+
+ 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))
}
- 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
+
+ if (deletions.isNotEmpty()) {
+ JobQueue.shared.add(
+ openGroupDeleteJobFactory.create(
+ messageServerIds = LongArray(deletions.size) { i -> deletions[i].id },
+ threadId = threadId
+ )
)
- })
- handleDeletedMessages(server, roomToken, deletions.map { it.id })
+ }
}
- private suspend fun handleDirectMessages(
- server: String,
- fromOutbox: Boolean,
- messages: List
+ /**
+ * Handle messages that are sent to us directly.
+ */
+ private fun handleInboxMessages(
+ 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)
- }
- 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 sorted = messages.sortedBy { it.postedAt }
- 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)
+ val serverPubKeyHex = storage.getOpenGroupPublicKey(server)
+ ?: run {
+ Log.e(TAG, "No community server public key cannot process inbox messages")
+ return
}
- }
- }
- private fun handleNewMessages(server: String, roomToken: String, messages: 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))
+ receivedMessageProcessor.startProcessing("CommunityInbox") { ctx ->
+ for (apiMessage in sorted) {
+ try {
+ storage.setLastInboxMessageId(server, sorted.last().id)
+
+ receivedMessageProcessor.processCommunityInboxMessage(
+ context = ctx,
+ message = apiMessage,
+ communityServerUrl = server,
+ communityServerPubKeyHex = serverPubKeyHex,
+ )
+
+ } catch (e: Exception) {
+ Log.e(TAG, "Error processing inbox message", e)
+ }
}
}
+ }
+
+ /**
+ * Handle messages that we have sent out to others.
+ */
+ private fun handleOutboxMessages(
+ messages: List
+ ) {
+ if (messages.isEmpty()) return
+ val sorted = messages.sortedBy { it.postedAt }
- envelopes.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { list ->
- val parameters = list.map { (serverId, message, reactions) ->
- MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId, reactions = reactions)
+ val serverPubKeyHex = storage.getOpenGroupPublicKey(server)
+ ?: run {
+ Log.e(TAG, "No community server public key cannot process inbox messages")
+ return
}
- JobQueue.shared.add(batchMessageJobFactory.create(
- parameters,
- fromCommunity = threadAddress
- ))
- }
- if (envelopes.isNotEmpty()) {
- JobQueue.shared.add(trimThreadJobFactory.create(threadId))
- }
- }
+ receivedMessageProcessor.startProcessing("CommunityOutbox") { ctx ->
+ for (apiMessage in sorted) {
+ try {
+ storage.setLastOutboxMessageId(server, sorted.last().id)
- private fun handleDeletedMessages(server: String, roomToken: String, serverIds: List) {
- val threadID = storage.getThreadId(Address.Community(serverUrl = server, room = roomToken)) ?: return
+ receivedMessageProcessor.processCommunityOutboxMessage(
+ context = ctx,
+ msg = apiMessage,
+ communityServerUrl = server,
+ communityServerPubKeyHex = serverPubKeyHex,
+ )
- if (serverIds.isNotEmpty()) {
- JobQueue.shared.add(
- openGroupDeleteJobFactory.create(
- messageServerIds = serverIds.toLongArray(),
- threadId = threadID
- )
- )
+ } catch (e: Exception) {
+ Log.e(TAG, "Error processing outbox message", e)
+ }
+ }
}
}
+
sealed interface PollState {
data class Idle(val lastPolled: Result>?) : PollState
data object Polling : PollState
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerManager.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerManager.kt
index 463e3509f7..373c5b8fe7 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerManager.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerManager.kt
@@ -6,8 +6,6 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan
@@ -21,6 +19,7 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.UserConfigType
import org.session.libsession.utilities.userConfigsChanged
import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.auth.LoginStateRepository
import org.thoughtcrime.securesms.dependencies.ManagerScope
import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent
import org.thoughtcrime.securesms.util.castAwayType
@@ -44,29 +43,23 @@ class OpenGroupPollerManager @Inject constructor(
pollerFactory: OpenGroupPoller.Factory,
configFactory: ConfigFactoryProtocol,
preferences: TextSecurePreferences,
+ loginStateRepository: LoginStateRepository,
@ManagerScope scope: CoroutineScope
) : OnAppStartupComponent {
private val pollerSemaphore = Semaphore(3)
val pollers: StateFlow