diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTest.kt deleted file mode 100644 index 4a28ecc122..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package org.thoughtcrime.securesms.messages - -import android.app.Application -import androidx.test.core.app.ApplicationProvider -import org.junit.Rule -import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata -import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.testing.SignalActivityRule -import org.thoughtcrime.securesms.testing.TestProtos -import org.whispersystems.signalservice.api.messages.SignalServiceContent -import org.whispersystems.signalservice.internal.push.SignalServiceProtos -import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto - -abstract class MessageContentProcessorTest { - - @get:Rule - val harness = SignalActivityRule() - - protected fun MessageContentProcessor.doProcess( - messageState: MessageState = MessageState.DECRYPTED_OK, - content: SignalServiceContent, - exceptionMetadata: ExceptionMetadata = ExceptionMetadata("sender", 1), - timestamp: Long = 100L, - smsMessageId: Long = -1L - ) { - process(messageState, content, exceptionMetadata, timestamp, smsMessageId) - } - - protected fun createNormalContentTestSubject(): MessageContentProcessor { - val context = ApplicationProvider.getApplicationContext() - - return MessageContentProcessor.create(context) - } - - /** - * Creates a valid ServiceContentProto with a data message which can be built via - * `injectDataMessage`. This function is intended to be built on-top of for more - * specific scenario in subclasses. - * - * Example can be seen in __handleStoryMessageTest - */ - protected fun createServiceContentWithDataMessage( - messageSender: Recipient = Recipient.resolved(harness.others.first()), - injectDataMessage: SignalServiceProtos.DataMessage.Builder.() -> Unit - ): SignalServiceContentProto { - return TestProtos.build { - serviceContent( - localAddress = address(uuid = harness.self.requireServiceId().rawUuid).build(), - metadata = metadata( - address = address(uuid = messageSender.requireServiceId().rawUuid).build() - ).build() - ).apply { - content = content().apply { - dataMessage = dataMessage().apply { - injectDataMessage() - }.build() - }.build() - }.build() - } - } -} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleStoryMessageTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleStoryMessageTest.kt deleted file mode 100644 index 8a5a6d562f..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleStoryMessageTest.kt +++ /dev/null @@ -1,181 +0,0 @@ -package org.thoughtcrime.securesms.messages - -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.signal.core.util.requireLong -import org.signal.libsignal.zkgroup.groups.GroupMasterKey -import org.signal.storageservice.protos.groups.Member -import org.signal.storageservice.protos.groups.local.DecryptedGroup -import org.signal.storageservice.protos.groups.local.DecryptedMember -import org.thoughtcrime.securesms.database.MessageTable -import org.thoughtcrime.securesms.database.MmsHelper -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.DistributionListId -import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord -import org.thoughtcrime.securesms.database.model.ParentStoryId -import org.thoughtcrime.securesms.database.model.StoryType -import org.thoughtcrime.securesms.mms.IncomingMediaMessage -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.testing.TestProtos -import org.whispersystems.signalservice.api.messages.SignalServiceContent -import org.whispersystems.signalservice.api.push.DistributionId -import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage -import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto -import kotlin.random.Random - -@Suppress("ClassName") -class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorTest() { - - @Before - fun setUp() { - SignalDatabase.messages.deleteAllThreads() - } - - @After - fun tearDown() { - SignalDatabase.messages.deleteAllThreads() - } - - @Test - fun givenContentWithADirectStoryReplyWhenIProcessThenIInsertAReplyInTheCorrectThread() { - val sender = Recipient.resolved(harness.others.first()) - val senderThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(sender) - val myStory = Recipient.resolved(SignalDatabase.distributionLists.getRecipientId(DistributionListId.MY_STORY)!!) - val myStoryThread = SignalDatabase.threads.getOrCreateThreadIdFor(myStory) - val expectedSentTime = 200L - val storyMessageId = MmsHelper.insert( - sentTimeMillis = expectedSentTime, - recipient = myStory, - storyType = StoryType.STORY_WITH_REPLIES, - threadId = myStoryThread - ) - - SignalDatabase.storySends.insert( - messageId = storyMessageId, - recipientIds = listOf(sender.id), - sentTimestamp = expectedSentTime, - allowsReplies = true, - distributionId = DistributionId.MY_STORY - ) - - val expectedBody = "Hello!" - - val storyContent: SignalServiceContentProto = createServiceContentWithStoryContext( - messageSender = sender, - storyAuthor = harness.self, - storySentTimestamp = expectedSentTime - ) { - body = expectedBody - } - - runTestWithContent(contentProto = storyContent) - - val replyId = SignalDatabase.messages.getConversation(senderThreadId, 0, 1).use { - it.moveToFirst() - it.requireLong(MessageTable.ID) - } - - val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord - assertEquals(ParentStoryId.DirectReply(storyMessageId).serialize(), replyRecord.parentStoryId!!.serialize()) - assertEquals(expectedBody, replyRecord.body) - - SignalDatabase.messages.deleteAllThreads() - } - - @Test - fun givenContentWithAGroupStoryReplyWhenIProcessThenIInsertAReplyToTheCorrectStory() { - val sender = Recipient.resolved(harness.others[0]) - val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE)) - val decryptedGroupState = DecryptedGroup.newBuilder() - .addAllMembers( - listOf( - DecryptedMember.newBuilder() - .setAciBytes(harness.self.requireAci().toByteString()) - .setJoinedAtRevision(0) - .setRole(Member.Role.DEFAULT) - .build(), - DecryptedMember.newBuilder() - .setAciBytes(sender.requireAci().toByteString()) - .setJoinedAtRevision(0) - .setRole(Member.Role.DEFAULT) - .build() - ) - ) - .setRevision(0) - .build() - - val group = SignalDatabase.groups.create( - groupMasterKey, - decryptedGroupState - ) - - val groupRecipient = Recipient.externalGroupExact(group!!) - val threadForGroup = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient) - - val insertResult = MmsHelper.insert( - message = IncomingMediaMessage( - from = sender.id, - sentTimeMillis = 100L, - serverTimeMillis = 101L, - receivedTimeMillis = 102L, - storyType = StoryType.STORY_WITH_REPLIES - ), - threadId = threadForGroup - ) - - val expectedBody = "Hello, World!" - val storyContent: SignalServiceContentProto = createServiceContentWithStoryContext( - messageSender = sender, - storyAuthor = sender, - storySentTimestamp = 100L - ) { - groupV2 = TestProtos.build { groupContextV2(masterKeyBytes = groupMasterKey.serialize()).build() } - body = expectedBody - } - - runTestWithContent(storyContent) - - val replyId = SignalDatabase.messages.getStoryReplies(insertResult.get().messageId).use { cursor -> - assertEquals(1, cursor.count) - cursor.moveToFirst() - cursor.requireLong(MessageTable.ID) - } - - val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord - assertEquals(ParentStoryId.GroupReply(insertResult.get().messageId).serialize(), replyRecord.parentStoryId?.serialize()) - assertEquals(threadForGroup, replyRecord.threadId) - assertEquals(expectedBody, replyRecord.body) - - SignalDatabase.messages.deleteGroupStoryReplies(insertResult.get().messageId) - SignalDatabase.messages.deleteAllThreads() - } - - /** - * Creates a ServiceContent proto with a StoryContext, and then - * uses `injectDataMessage` to fill in the data message object. - */ - private fun createServiceContentWithStoryContext( - messageSender: Recipient, - storyAuthor: Recipient, - storySentTimestamp: Long, - injectDataMessage: DataMessage.Builder.() -> Unit - ): SignalServiceContentProto { - return createServiceContentWithDataMessage(messageSender) { - storyContext = TestProtos.build { - storyContext( - sentTimestamp = storySentTimestamp, - authorUuid = storyAuthor.requireServiceId().toString() - ).build() - } - injectDataMessage() - } - } - - private fun runTestWithContent(contentProto: SignalServiceContentProto) { - val content = SignalServiceContent.createFromProto(contentProto) - val testSubject = createNormalContentTestSubject() - testSubject.doProcess(content = content!!) - } -} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleTextMessageTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleTextMessageTest.kt deleted file mode 100644 index 5994e4ad95..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleTextMessageTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.thoughtcrime.securesms.messages - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test -import org.thoughtcrime.securesms.database.SignalDatabase -import org.whispersystems.signalservice.api.messages.SignalServiceContent -import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto - -@Suppress("ClassName") -class MessageContentProcessor__handleTextMessageTest : MessageContentProcessorTest() { - @Test - fun givenContentWithATextMessageWhenIProcessThenIInsertTheTextMessage() { - val testSubject: MessageContentProcessor = createNormalContentTestSubject() - val expectedBody = "Hello, World!" - val contentProto: SignalServiceContentProto = createServiceContentWithDataMessage { - body = expectedBody - } - - val content = SignalServiceContent.createFromProto(contentProto) - - // WHEN - testSubject.doProcess(content = content!!) - - // THEN - val record = SignalDatabase.messages.getMessageRecord(1) - val threadSize = SignalDatabase.messages.getMessageCountForThread(record.threadId) - assertEquals(1, threadSize) - - assertTrue(record.isSecure) - assertEquals(expectedBody, record.body) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PushTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/PushTable.java deleted file mode 100644 index c99c4a08c8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/PushTable.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.thoughtcrime.securesms.database; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; - -import androidx.annotation.NonNull; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.util.Base64; -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.internal.util.Util; - -import java.io.Closeable; -import java.io.IOException; -import java.util.Optional; - -public class PushTable extends DatabaseTable { - - private static final String TAG = Log.tag(PushTable.class); - - private static final String TABLE_NAME = "push"; - public static final String ID = "_id"; - public static final String TYPE = "type"; - public static final String SOURCE_E164 = "source"; - public static final String SOURCE_UUID = "source_uuid"; - public static final String DEVICE_ID = "device_id"; - public static final String LEGACY_MSG = "body"; - public static final String CONTENT = "content"; - public static final String TIMESTAMP = "timestamp"; - public static final String SERVER_RECEIVED_TIMESTAMP = "server_timestamp"; - public static final String SERVER_DELIVERED_TIMESTAMP = "server_delivered_timestamp"; - public static final String SERVER_GUID = "server_guid"; - - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + - TYPE + " INTEGER, " + - SOURCE_E164 + " TEXT, " + - SOURCE_UUID + " TEXT, " + - DEVICE_ID + " INTEGER, " + - LEGACY_MSG + " TEXT, " + - CONTENT + " TEXT, " + - TIMESTAMP + " INTEGER, " + - SERVER_RECEIVED_TIMESTAMP + " INTEGER DEFAULT 0, " + - SERVER_DELIVERED_TIMESTAMP + " INTEGER DEFAULT 0, " + - SERVER_GUID + " TEXT DEFAULT NULL);"; - - public PushTable(Context context, SignalDatabase databaseHelper) { - super(context, databaseHelper); - } - - public long insert(@NonNull SignalServiceEnvelope envelope) { - Optional messageId = find(envelope); - - if (messageId.isPresent()) { - return -1; - } else { - ContentValues values = new ContentValues(); - values.put(TYPE, envelope.getType()); - values.put(SOURCE_UUID, envelope.getSourceServiceId().orElse(null)); - values.put(DEVICE_ID, envelope.getSourceDevice()); - values.put(CONTENT, envelope.hasContent() ? Base64.encodeBytes(envelope.getContent()) : ""); - values.put(TIMESTAMP, envelope.getTimestamp()); - values.put(SERVER_RECEIVED_TIMESTAMP, envelope.getServerReceivedTimestamp()); - values.put(SERVER_DELIVERED_TIMESTAMP, envelope.getServerDeliveredTimestamp()); - values.put(SERVER_GUID, envelope.getServerGuid()); - - return databaseHelper.getSignalWritableDatabase().insert(TABLE_NAME, null, values); - } - } - - public SignalServiceEnvelope get(long id) throws NoSuchMessageException { - Cursor cursor = null; - - try { - cursor = databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, ID_WHERE, - new String[] {String.valueOf(id)}, - null, null, null); - - if (cursor != null && cursor.moveToNext()) { - String legacyMessage = cursor.getString(cursor.getColumnIndexOrThrow(LEGACY_MSG)); - String content = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT)); - String uuid = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_UUID)); - String e164 = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_E164)); - - return new SignalServiceEnvelope(cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)), - SignalServiceAddress.fromRaw(uuid, e164), - cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID)), - cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)), - Util.isEmpty(content) ? null : Base64.decode(content), - cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_RECEIVED_TIMESTAMP)), - cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_DELIVERED_TIMESTAMP)), - cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID)), - "", - true, - false, - null); - } - } catch (IOException e) { - Log.w(TAG, e); - throw new NoSuchMessageException(e); - } finally { - if (cursor != null) - cursor.close(); - } - - throw new NoSuchMessageException("Not found"); - } - - public Cursor getPending() { - return databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null); - } - - public void delete(long id) { - databaseHelper.getSignalWritableDatabase().delete(TABLE_NAME, ID_WHERE, new String[] {id+""}); - } - - public Reader readerFor(Cursor cursor) { - return new Reader(cursor); - } - - private Optional find(SignalServiceEnvelope envelope) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String query = TYPE + " = ? AND " + - DEVICE_ID + " = ? AND " + - LEGACY_MSG + " = ? AND " + - CONTENT + " = ? AND " + - TIMESTAMP + " = ? AND " + - "(" + SOURCE_UUID + " NOT NULL AND " + SOURCE_UUID + " = ?)"; - - String[] args = new String[] { String.valueOf(envelope.getType()), - String.valueOf(envelope.getSourceDevice()), - envelope.hasContent() ? Base64.encodeBytes(envelope.getContent()) : "", - String.valueOf(envelope.getTimestamp()), - String.valueOf(envelope.getSourceServiceId().orElse(null)) }; - - - try (Cursor cursor = database.query(TABLE_NAME, null, query, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - return Optional.of(cursor.getLong(cursor.getColumnIndexOrThrow(ID))); - } else { - return Optional.empty(); - } - } - } - - public static class Reader implements Closeable { - private final Cursor cursor; - - public Reader(Cursor cursor) { - this.cursor = cursor; - } - - public SignalServiceEnvelope getNext() { - try { - if (cursor == null || !cursor.moveToNext()) - return null; - - int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)); - String sourceUuid = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_UUID)); - String sourceE164 = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_E164)); - int deviceId = cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID)); - String content = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT)); - long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); - long serverReceivedTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_RECEIVED_TIMESTAMP)); - long serverDeliveredTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_DELIVERED_TIMESTAMP)); - String serverGuid = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID)); - - return new SignalServiceEnvelope(type, - SignalServiceAddress.fromRaw(sourceUuid, sourceE164), - deviceId, - timestamp, - content != null ? Base64.decode(content) : null, - serverReceivedTimestamp, - serverDeliveredTimestamp, - serverGuid, - "", - true, - false, - null); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - @Override - public void close() { - this.cursor.close(); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt index 4a4898b8a8..fbc238f1f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt @@ -44,7 +44,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data val threadTable: ThreadTable = ThreadTable(context, this) val identityTable: IdentityTable = IdentityTable(context, this) val draftTable: DraftTable = DraftTable(context, this) - val pushTable: PushTable = PushTable(context, this) val groupTable: GroupTable = GroupTable(context, this) val recipientTable: RecipientTable = RecipientTable(context, this) val groupReceiptTable: GroupReceiptTable = GroupReceiptTable(context, this) @@ -86,7 +85,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data db.execSQL(ThreadTable.CREATE_TABLE) db.execSQL(IdentityTable.CREATE_TABLE) db.execSQL(DraftTable.CREATE_TABLE) - db.execSQL(PushTable.CREATE_TABLE) executeStatements(db, GroupTable.CREATE_TABLES) db.execSQL(RecipientTable.CREATE_TABLE) db.execSQL(GroupReceiptTable.CREATE_TABLE) @@ -467,12 +465,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data val pendingPniSignatureMessages: PendingPniSignatureMessageTable get() = instance!!.pendingPniSignatureMessageTable - @get:Deprecated("This only exists to migrate from legacy storage. There shouldn't be any new usages.") - @get:JvmStatic - @get:JvmName("push") - val push: PushTable - get() = instance!!.pushTable - @get:JvmStatic @get:JvmName("recipients") val recipients: RecipientTable diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java index f3c1a70ff1..9ddecdc9ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java @@ -20,6 +20,7 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; import com.google.i18n.phonenumbers.ShortNumberInfo; +import org.signal.core.util.Hex; import org.signal.core.util.StreamUtil; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.IdentityKey; @@ -31,12 +32,11 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.database.AttachmentTable; import org.thoughtcrime.securesms.database.DraftTable; -import org.thoughtcrime.securesms.database.GroupTable; import org.thoughtcrime.securesms.database.GroupReceiptTable; +import org.thoughtcrime.securesms.database.GroupTable; import org.thoughtcrime.securesms.database.IdentityTable; -import org.thoughtcrime.securesms.database.MessageTypes; import org.thoughtcrime.securesms.database.MessageTable; -import org.thoughtcrime.securesms.database.PushTable; +import org.thoughtcrime.securesms.database.MessageTypes; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.ThreadTable; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -46,7 +46,6 @@ import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.phonenumbers.NumberUtil; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.DelimiterUtil; -import org.signal.core.util.Hex; import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -133,7 +132,6 @@ public class ClassicOpenHelper extends SQLiteOpenHelper { db.execSQL(ThreadTable.CREATE_TABLE); db.execSQL(IdentityTable.CREATE_TABLE); db.execSQL(DraftTable.CREATE_TABLE); - db.execSQL(PushTable.CREATE_TABLE); db.execSQL(GroupTable.CREATE_TABLE); db.execSQL(RecipientTable.CREATE_TABLE); db.execSQL(GroupReceiptTable.CREATE_TABLE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 918ce7fb47..f7a99fc370 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -60,6 +60,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V201_RecipientTable import org.thoughtcrime.securesms.database.helpers.migration.V202_DropMessageTableThreadDateIndex import org.thoughtcrime.securesms.database.helpers.migration.V203_PreKeyStaleTimestamp import org.thoughtcrime.securesms.database.helpers.migration.V204_GroupForeignKeyMigration +import org.thoughtcrime.securesms.database.helpers.migration.V205_DropPushTable /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -68,7 +69,7 @@ object SignalDatabaseMigrations { val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass) - const val DATABASE_VERSION = 204 + const val DATABASE_VERSION = 205 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -295,6 +296,10 @@ object SignalDatabaseMigrations { if (oldVersion < 204) { V204_GroupForeignKeyMigration.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 205) { + V205_DropPushTable.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V205_DropPushTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V205_DropPushTable.kt new file mode 100644 index 0000000000..2c42b59f30 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V205_DropPushTable.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +/** + * Drop the no longer used push table. + */ +@Suppress("ClassName") +object V205_DropPushTable : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("DROP TABLE IF EXISTS push") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index d94e0de04a..23f009266b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -35,13 +35,11 @@ import org.thoughtcrime.securesms.jobmanager.JobMigrator; import org.thoughtcrime.securesms.jobmanager.impl.FactoryJobPredicate; import org.thoughtcrime.securesms.jobs.FastJobStorage; import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob; +import org.thoughtcrime.securesms.jobs.IndividualSendJob; import org.thoughtcrime.securesms.jobs.JobManagerFactories; import org.thoughtcrime.securesms.jobs.MarkerJob; import org.thoughtcrime.securesms.jobs.PreKeysSyncJob; -import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob; import org.thoughtcrime.securesms.jobs.PushGroupSendJob; -import org.thoughtcrime.securesms.jobs.IndividualSendJob; -import org.thoughtcrime.securesms.jobs.PushProcessMessageJob; import org.thoughtcrime.securesms.jobs.PushProcessMessageJobV2; import org.thoughtcrime.securesms.jobs.ReactionSendJob; import org.thoughtcrime.securesms.jobs.TypingSendJob; @@ -172,7 +170,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr .setConstraintObservers(JobManagerFactories.getConstraintObservers(context)) .setJobStorage(new FastJobStorage(JobDatabase.getInstance(context))) .setJobMigrator(new JobMigrator(TextSecurePreferences.getJobManagerVersion(context), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(context))) - .addReservedJobRunner(new FactoryJobPredicate(PushDecryptMessageJob.KEY, PushProcessMessageJob.KEY, PushProcessMessageJobV2.KEY, MarkerJob.KEY)) + .addReservedJobRunner(new FactoryJobPredicate(PushProcessMessageJobV2.KEY, MarkerJob.KEY)) .addReservedJobRunner(new FactoryJobPredicate(IndividualSendJob.KEY, PushGroupSendJob.KEY, ReactionSendJob.KEY, TypingSendJob.KEY, GroupCallUpdateSendJob.KEY)) .build(); return new JobManager(context, config); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java index f7e177cbbd..20c843051d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java @@ -43,7 +43,7 @@ public class JobManager implements ConstraintObserver.Notifier { private static final String TAG = Log.tag(JobManager.class); - public static final int CURRENT_VERSION = 9; + public static final int CURRENT_VERSION = 10; private final Application application; private final Configuration configuration; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/PushDecryptMessageJobEnvelopeMigration.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/PushDecryptMessageJobEnvelopeMigration.java index 59a0980c17..b70bdd0fa8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/PushDecryptMessageJobEnvelopeMigration.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/PushDecryptMessageJobEnvelopeMigration.java @@ -1,60 +1,24 @@ package org.thoughtcrime.securesms.jobmanager.migrations; -import android.content.Context; - import androidx.annotation.NonNull; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.database.NoSuchMessageException; -import org.thoughtcrime.securesms.database.PushTable; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.JobMigration; -import org.thoughtcrime.securesms.jobs.FailingJob; -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; /** * We removed the messageId property from the job data and replaced it with a serialized envelope, * so we need to take jobs that referenced an ID and replace it with the envelope instead. + * + * @deprecated No longer have a PushDecryptJob to migrate, job now maps to {@link org.thoughtcrime.securesms.jobs.FailingJob} + * in {@link org.thoughtcrime.securesms.jobs.JobManagerFactories} */ public class PushDecryptMessageJobEnvelopeMigration extends JobMigration { - private static final String TAG = Log.tag(PushDecryptMessageJobEnvelopeMigration.class); - - private final PushTable pushDatabase; - - public PushDecryptMessageJobEnvelopeMigration(@NonNull Context context) { + public PushDecryptMessageJobEnvelopeMigration() { super(8); - this.pushDatabase = SignalDatabase.push(); } @Override protected @NonNull JobData migrate(@NonNull JobData jobData) { - if ("PushDecryptJob".equals(jobData.getFactoryKey())) { - Log.i(TAG, "Found a PushDecryptJob to migrate."); - return migratePushDecryptMessageJob(pushDatabase, jobData); - } else { - return jobData; - } - } - - private static @NonNull JobData migratePushDecryptMessageJob(@NonNull PushTable pushDatabase, @NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - if (data.hasLong("message_id")) { - long messageId = data.getLong("message_id"); - try { - SignalServiceEnvelope envelope = pushDatabase.get(messageId); - return jobData.withData(data.buildUpon() - .putBlobAsString("envelope", envelope.serialize()) - .serialize()); - } catch (NoSuchMessageException e) { - Log.w(TAG, "Failed to find envelope in DB! Failing."); - return jobData.withFactoryKey(FailingJob.KEY); - } - } else { - Log.w(TAG, "No message_id property?"); - return jobData; - } + return jobData; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/PushProcessMessageJobMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/PushProcessMessageJobMigration.kt new file mode 100644 index 0000000000..ae37b4775c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/PushProcessMessageJobMigration.kt @@ -0,0 +1,97 @@ +package org.thoughtcrime.securesms.jobmanager.migrations + +import okio.ByteString.Companion.toByteString +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.jobmanager.JobMigration +import org.thoughtcrime.securesms.jobmanager.JsonJobData +import org.thoughtcrime.securesms.jobs.FailingJob +import org.thoughtcrime.securesms.jobs.PushProcessMessageErrorV2Job +import org.thoughtcrime.securesms.messages.MessageState +import org.thoughtcrime.securesms.util.Base64 +import org.whispersystems.signalservice.api.crypto.protos.CompleteMessage +import org.whispersystems.signalservice.api.crypto.protos.EnvelopeMetadata +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope +import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto + +/** + * Migrate PushProcessMessageJob V1 to V2 versions. + */ +class PushProcessMessageJobMigration : JobMigration(10) { + override fun migrate(jobData: JobData): JobData { + return if ("PushProcessJob" == jobData.factoryKey) { + migrateJob(jobData) + } else { + jobData + } + } + + companion object { + private val TAG = Log.tag(PushProcessMessageJobMigration::class.java) + + @Suppress("MoveVariableDeclarationIntoWhen") + private fun migrateJob(jobData: JobData): JobData { + val data = JsonJobData.deserialize(jobData.data) + return if (data.hasInt("message_state")) { + val state = MessageState.values()[data.getInt("message_state")] + return when (state) { + MessageState.NOOP -> jobData.withFactoryKey(FailingJob.KEY) + + MessageState.DECRYPTED_OK -> { + try { + migratePushProcessJobWithDecryptedData(jobData, data) + } catch (t: Throwable) { + Log.w(TAG, "Unable to migrate successful process job", t) + jobData.withFactoryKey(FailingJob.KEY) + } + } + + else -> { + Log.i(TAG, "Migrating push process error job for state: $state") + jobData.withFactoryKey(PushProcessMessageErrorV2Job.KEY) + } + } + } else { + jobData.withFactoryKey(FailingJob.KEY) + } + } + + private fun migratePushProcessJobWithDecryptedData(jobData: JobData, inputData: JsonJobData): JobData { + Log.i(TAG, "Migrating PushProcessJob to V2") + + val protoBytes: ByteArray = Base64.decode(inputData.getString("message_content")) + val proto = SignalServiceContentProto.parseFrom(protoBytes) + + val sourceServiceId = ServiceId.parseOrThrow(proto.metadata.address.uuid) + val destinationServiceId = ServiceId.parseOrThrow(proto.metadata.destinationUuid) + + val envelope = Envelope.newBuilder() + .setSourceServiceId(sourceServiceId.toString()) + .setSourceDevice(proto.metadata.senderDevice) + .setDestinationServiceId(destinationServiceId.toString()) + .setTimestamp(proto.metadata.timestamp) + .setServerGuid(proto.metadata.serverGuid) + .setServerTimestamp(proto.metadata.serverReceivedTimestamp) + + val metadata = EnvelopeMetadata( + sourceServiceId = sourceServiceId.toByteArray().toByteString(), + sourceE164 = if (proto.metadata.address.hasE164()) proto.metadata.address.e164 else null, + sourceDeviceId = proto.metadata.senderDevice, + sealedSender = proto.metadata.needsReceipt, + groupId = if (proto.metadata.hasGroupId()) proto.metadata.groupId.toByteArray().toByteString() else null, + destinationServiceId = destinationServiceId.toByteArray().toByteString() + ) + + val completeMessage = CompleteMessage( + envelope = envelope.build().toByteArray().toByteString(), + content = proto.content.toByteArray().toByteString(), + metadata = metadata, + serverDeliveredTimestamp = proto.metadata.serverDeliveredTimestamp + ) + + return jobData + .withFactoryKey("PushProcessMessageJobV2") + .withData(completeMessage.encode()) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java index 84b8ac0721..dfea17ed23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java @@ -50,7 +50,7 @@ public class AutomaticSessionResetJob extends BaseJob { public AutomaticSessionResetJob(@NonNull RecipientId recipientId, int deviceId, long sentTimestamp) { this(new Parameters.Builder() - .setQueue(PushProcessMessageJob.getQueueName(recipientId)) + .setQueue(PushProcessMessageJobV2.getQueueName(recipientId)) .addConstraint(DecryptionsDrainedConstraint.KEY) .setMaxInstancesForQueue(1) .build(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkPeekJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkPeekJob.kt index 8793c7c534..641aba2483 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkPeekJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkPeekJob.kt @@ -37,7 +37,7 @@ internal class CallLinkPeekJob private constructor( constructor(callLinkRecipientId: RecipientId) : this( Parameters.Builder() - .setQueue(PushProcessMessageJob.getQueueName(callLinkRecipientId)) + .setQueue(PushProcessMessageJobV2.getQueueName(callLinkRecipientId)) .setMaxInstancesForQueue(1) .setLifespan(TimeUnit.MINUTES.toMillis(1)) .addConstraint(NetworkConstraint.KEY) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2WorkerJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2WorkerJob.java index 872ae332db..fa0d64a361 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2WorkerJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ForceUpdateGroupV2WorkerJob.java @@ -37,7 +37,7 @@ final class ForceUpdateGroupV2WorkerJob extends BaseJob { private final GroupId.V2 groupId; ForceUpdateGroupV2WorkerJob(@NonNull GroupId.V2 groupId) { - this(new Parameters.Builder().setQueue(PushProcessMessageJob.getQueueName(Recipient.externalGroupExact(groupId).getId())) + this(new Parameters.Builder().setQueue(PushProcessMessageJobV2.getQueueName(Recipient.externalGroupExact(groupId).getId())) .addConstraint(NetworkConstraint.KEY) .setMaxAttempts(Parameters.UNLIMITED) .build(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekWorkerJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekWorkerJob.java index 226687a5b0..e9fe5bc6fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekWorkerJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallPeekWorkerJob.java @@ -21,7 +21,7 @@ final class GroupCallPeekWorkerJob extends BaseJob { public GroupCallPeekWorkerJob(@NonNull RecipientId groupRecipientId) { this(new Parameters.Builder() - .setQueue(PushProcessMessageJob.getQueueName(groupRecipientId)) + .setQueue(PushProcessMessageJobV2.getQueueName(groupRecipientId)) .setMaxInstancesForQueue(2) .build(), groupRecipientId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 6a1d1287ce..c7417fb610 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; import org.thoughtcrime.securesms.jobmanager.migrations.PushDecryptMessageJobEnvelopeMigration; +import org.thoughtcrime.securesms.jobmanager.migrations.PushProcessMessageJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.PushProcessMessageQueueJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMigration2; @@ -47,7 +48,6 @@ import org.thoughtcrime.securesms.migrations.BlobStorageLocationMigrationJob; import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob; import org.thoughtcrime.securesms.migrations.ClearGlideCacheMigrationJob; import org.thoughtcrime.securesms.migrations.DatabaseMigrationJob; -import org.thoughtcrime.securesms.migrations.DecryptionsDrainedMigrationJob; import org.thoughtcrime.securesms.migrations.DeleteDeprecatedLogsMigrationJob; import org.thoughtcrime.securesms.migrations.DirectoryRefreshMigrationJob; import org.thoughtcrime.securesms.migrations.EmojiDownloadMigrationJob; @@ -165,14 +165,12 @@ public final class JobManagerFactories { put(PreKeysSyncJob.KEY, new PreKeysSyncJob.Factory()); put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory()); put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory()); - put(PushDecryptMessageJob.KEY, new PushDecryptMessageJob.Factory()); - put(PushDecryptDrainedJob.KEY, new PushDecryptDrainedJob.Factory()); put(PushDistributionListSendJob.KEY, new PushDistributionListSendJob.Factory()); put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory()); put(PushGroupSilentUpdateSendJob.KEY, new PushGroupSilentUpdateSendJob.Factory()); put(MessageFetchJob.KEY, new MessageFetchJob.Factory()); put(PushProcessEarlyMessagesJob.KEY, new PushProcessEarlyMessagesJob.Factory()); - put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory()); + put(PushProcessMessageErrorV2Job.KEY, new PushProcessMessageErrorV2Job.Factory()); put(PushProcessMessageJobV2.KEY, new PushProcessMessageJobV2.Factory()); put(ReactionSendJob.KEY, new ReactionSendJob.Factory()); put(RebuildMessageSearchIndexJob.KEY, new RebuildMessageSearchIndexJob.Factory()); @@ -234,7 +232,6 @@ public final class JobManagerFactories { put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory()); put(ClearGlideCacheMigrationJob.KEY, new ClearGlideCacheMigrationJob.Factory()); put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory()); - put(DecryptionsDrainedMigrationJob.KEY, new DecryptionsDrainedMigrationJob.Factory()); put(DeleteDeprecatedLogsMigrationJob.KEY, new DeleteDeprecatedLogsMigrationJob.Factory()); put(DirectoryRefreshMigrationJob.KEY, new DirectoryRefreshMigrationJob.Factory()); put(EmojiDownloadMigrationJob.KEY, new EmojiDownloadMigrationJob.Factory()); @@ -295,6 +292,10 @@ public final class JobManagerFactories { put("KbsEnclaveMigrationWorkerJob", new FailingJob.Factory()); put("KbsEnclaveMigrationJob", new PassingMigrationJob.Factory()); put("ClearFallbackKbsEnclaveJob", new FailingJob.Factory()); + put("PushDecryptJob", new FailingJob.Factory()); + put("PushDecryptDrainedJob", new FailingJob.Factory()); + put("PushProcessJob", new FailingJob.Factory()); + put("DecryptionsDrainedMigrationJob", new PassingMigrationJob.Factory()); }}; } @@ -331,7 +332,8 @@ public final class JobManagerFactories { new SendReadReceiptsJobMigration(SignalDatabase.messages()), new PushProcessMessageQueueJobMigration(application), new RetrieveProfileJobMigration(), - new PushDecryptMessageJobEnvelopeMigration(application), - new SenderKeyDistributionSendJobRecipientMigration()); + new PushDecryptMessageJobEnvelopeMigration(), + new SenderKeyDistributionSendJobRecipientMigration(), + new PushProcessMessageJobMigration()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2WorkerJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2WorkerJob.kt index 1226917b93..508270db27 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2WorkerJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2WorkerJob.kt @@ -20,7 +20,7 @@ class LeaveGroupV2WorkerJob(parameters: Parameters, private val groupId: GroupId constructor(groupId: GroupId.V2) : this( parameters = Parameters.Builder() - .setQueue(PushProcessMessageJob.getQueueName(Recipient.externalGroupExact(groupId).id)) + .setQueue(PushProcessMessageJobV2.getQueueName(Recipient.externalGroupExact(groupId).id)) .addConstraint(NetworkConstraint.KEY) .setMaxAttempts(Parameters.UNLIMITED) .setMaxInstancesForQueue(2) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptDrainedJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptDrainedJob.java deleted file mode 100644 index b0e345ee99..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptDrainedJob.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.Job; - -/** - * A job that has the same queue as {@link PushDecryptMessageJob} that we enqueue so we can notify - * the {@link org.thoughtcrime.securesms.messages.IncomingMessageObserver} when the decryption job - * queue is empty. - */ -public class PushDecryptDrainedJob extends BaseJob { - - public static final String KEY = "PushDecryptDrainedJob"; - - private static final String TAG = Log.tag(PushDecryptDrainedJob.class); - - public PushDecryptDrainedJob() { - this(new Parameters.Builder() - .setQueue(PushDecryptMessageJob.QUEUE) - .build()); - } - - private PushDecryptDrainedJob(@NonNull Parameters parameters) { - super(parameters); - } - - @Override - public @Nullable byte[] serialize() { - return null; - } - - @Override - protected void onRun() throws Exception { - Log.i(TAG, "Decryptions are caught-up."); - ApplicationDependencies.getIncomingMessageObserver().notifyDecryptionsDrained(); - } - - @Override - protected boolean onShouldRetry(@NonNull Exception e) { - return false; - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onFailure() { - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull PushDecryptDrainedJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { - return new PushDecryptDrainedJob(parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.kt deleted file mode 100644 index a949d86e07..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.kt +++ /dev/null @@ -1,198 +0,0 @@ -package org.thoughtcrime.securesms.jobs - -import android.app.PendingIntent -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import org.signal.core.util.PendingIntentFlags.mutable -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.MainActivity -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobmanager.JsonJobData -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata -import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState -import org.thoughtcrime.securesms.messages.MessageDecryptor -import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore -import org.thoughtcrime.securesms.notifications.NotificationChannels -import org.thoughtcrime.securesms.notifications.NotificationIds -import org.thoughtcrime.securesms.transport.RetryLaterException -import org.thoughtcrime.securesms.util.TextSecurePreferences -import org.whispersystems.signalservice.api.messages.SignalServiceContent -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope -import org.whispersystems.signalservice.api.messages.SignalServiceMetadata -import org.whispersystems.signalservice.api.push.SignalServiceAddress -import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer -import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer -import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto -import java.util.Optional - -/** - * Decrypts an envelope. Enqueues a separate job, [PushProcessMessageJob], to actually insert - * the result into our database. - */ -class PushDecryptMessageJob private constructor( - parameters: Parameters, - private val envelope: SignalServiceEnvelope, - private val smsMessageId: Long -) : BaseJob(parameters) { - - companion object { - val TAG = Log.tag(PushDecryptMessageJob::class.java) - - const val KEY = "PushDecryptJob" - const val QUEUE = "__PUSH_DECRYPT_JOB__" - - private const val KEY_SMS_MESSAGE_ID = "sms_message_id" - private const val KEY_ENVELOPE = "envelope" - } - - @Deprecated("No more jobs of this type should be enqueued. Decryptions now happen as things come off of the websocket.") - @JvmOverloads - constructor(envelope: SignalServiceEnvelope, smsMessageId: Long = -1) : this( - Parameters.Builder() - .setQueue(QUEUE) - .setMaxAttempts(Parameters.UNLIMITED) - .build(), - envelope, - smsMessageId - ) - - override fun shouldTrace() = true - - override fun serialize(): ByteArray? { - return JsonJobData.Builder() - .putBlobAsString(KEY_ENVELOPE, envelope.serialize()) - .putLong(KEY_SMS_MESSAGE_ID, smsMessageId) - .serialize() - } - - override fun getFactoryKey() = KEY - - @Throws(RetryLaterException::class) - public override fun onRun() { - if (needsMigration()) { - Log.w(TAG, "Migration is still needed.") - postMigrationNotification() - throw RetryLaterException() - } - - val bufferedProtocolStore = BufferedProtocolStore.create() - val result = MessageDecryptor.decrypt(context, bufferedProtocolStore, envelope.proto, envelope.serverDeliveredTimestamp) - bufferedProtocolStore.flushToDisk() - - when (result) { - is MessageDecryptor.Result.Success -> { - ApplicationDependencies.getJobManager().add( - PushProcessMessageJob( - result.toMessageState(), - result.toSignalServiceContent(), - null, - smsMessageId, - result.envelope.timestamp - ) - ) - } - - is MessageDecryptor.Result.Error -> { - ApplicationDependencies.getJobManager().add( - PushProcessMessageJob( - result.toMessageState(), - null, - result.errorMetadata.toExceptionMetadata(), - smsMessageId, - result.envelope.timestamp - ) - ) - } - - is MessageDecryptor.Result.Ignore -> { - // No action needed - } - - else -> { - throw AssertionError("Unexpected result! ${result.javaClass.simpleName}") - } - } - - result.followUpOperations.forEach { it.run() } - } - - public override fun onShouldRetry(exception: Exception): Boolean { - return exception is RetryLaterException - } - - override fun onFailure() = Unit - - private fun needsMigration(): Boolean { - return TextSecurePreferences.getNeedsSqlCipherMigration(context) - } - - private fun MessageDecryptor.Result.toMessageState(): MessageState { - return when (this) { - is MessageDecryptor.Result.DecryptionError -> MessageState.DECRYPTION_ERROR - is MessageDecryptor.Result.Ignore -> MessageState.NOOP - is MessageDecryptor.Result.InvalidVersion -> MessageState.INVALID_VERSION - is MessageDecryptor.Result.LegacyMessage -> MessageState.LEGACY_MESSAGE - is MessageDecryptor.Result.Success -> MessageState.DECRYPTED_OK - is MessageDecryptor.Result.UnsupportedDataMessage -> MessageState.UNSUPPORTED_DATA_MESSAGE - } - } - - private fun MessageDecryptor.Result.Success.toSignalServiceContent(): SignalServiceContent { - val localAddress = SignalServiceAddress(this.metadata.destinationServiceId, Optional.ofNullable(SignalStore.account().e164)) - val metadata = SignalServiceMetadata( - SignalServiceAddress(this.metadata.sourceServiceId, Optional.ofNullable(this.metadata.sourceE164)), - this.metadata.sourceDeviceId, - this.envelope.timestamp, - this.envelope.serverTimestamp, - this.serverDeliveredTimestamp, - this.metadata.sealedSender, - this.envelope.serverGuid, - Optional.ofNullable(this.metadata.groupId), - this.metadata.destinationServiceId.toString() - ) - - val contentProto = SignalServiceContentProto.newBuilder() - .setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress)) - .setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(metadata)) - .setContent(content) - .build() - - return SignalServiceContent.createFromProto(contentProto)!! - } - - private fun MessageDecryptor.ErrorMetadata.toExceptionMetadata(): ExceptionMetadata { - return ExceptionMetadata( - this.sender, - this.senderDevice, - this.groupId - ) - } - - private fun postMigrationNotification() { - val notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().messagesChannel) - .setSmallIcon(R.drawable.ic_notification) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message)) - .setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages)) - .setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), mutable())) - .setDefaults(NotificationCompat.DEFAULT_SOUND or NotificationCompat.DEFAULT_VIBRATE) - .build() - - NotificationManagerCompat.from(context).notify(NotificationIds.LEGACY_SQLCIPHER_MIGRATION, notification) - } - - class Factory : Job.Factory { - override fun create(parameters: Parameters, serializedData: ByteArray?): PushDecryptMessageJob { - val data = JsonJobData.deserialize(serializedData) - return PushDecryptMessageJob( - parameters, - SignalServiceEnvelope.deserialize(data.getStringAsBlob(KEY_ENVELOPE)), - data.getLong(KEY_SMS_MESSAGE_ID) - ) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessEarlyMessagesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessEarlyMessagesJob.kt index 2c0463a920..ef03f98750 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessEarlyMessagesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessEarlyMessagesJob.kt @@ -6,10 +6,8 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.ServiceMessageId import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.messages.MessageContentProcessor import org.thoughtcrime.securesms.messages.MessageContentProcessorV2 import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry -import org.whispersystems.signalservice.api.messages.SignalServiceContent /** * A job that should be enqueued whenever we process a message that we think has arrived "early" (see [org.thoughtcrime.securesms.util.EarlyMessageCache]). @@ -43,15 +41,9 @@ class PushProcessEarlyMessagesJob private constructor(parameters: Parameters) : Log.i(TAG, "There are ${earlyIds.size} items in the early message cache with matches.") for (id: ServiceMessageId in earlyIds) { - val contents: List? = ApplicationDependencies.getEarlyMessageCache().retrieve(id.sender, id.sentTimestamp).orNull() - val earlyEntries: List? = ApplicationDependencies.getEarlyMessageCache().retrieveV2(id.sender, id.sentTimestamp).orNull() + val earlyEntries: List? = ApplicationDependencies.getEarlyMessageCache().retrieve(id.sender, id.sentTimestamp).orNull() - if (contents != null) { - for (content: SignalServiceContent in contents) { - Log.i(TAG, "[${id.sentTimestamp}] Processing early content for $id") - MessageContentProcessor.create(context).processEarlyContent(MessageContentProcessor.MessageState.DECRYPTED_OK, content, null, id.sentTimestamp, -1) - } - } else if (earlyEntries != null) { + if (earlyEntries != null) { for (entry in earlyEntries) { Log.i(TAG, "[${id.sentTimestamp}] Processing early V2 content for $id") MessageContentProcessorV2.create(context).process(entry.envelope, entry.content, entry.metadata, entry.serverDeliveredTimestamp, processingEarlyContent = true) @@ -84,13 +76,13 @@ class PushProcessEarlyMessagesJob private constructor(parameters: Parameters) : const val KEY = "PushProcessEarlyMessageJob" /** - * Enqueues a job to run after the most-recently-enqueued [PushProcessMessageJob]. + * Enqueues a job to run after the most-recently-enqueued [PushProcessMessageJobV2]. */ @JvmStatic fun enqueue() { val jobManger = ApplicationDependencies.getJobManager() - val youngestProcessJobId: String? = jobManger.find { it.factoryKey == PushProcessMessageJob.KEY } + val youngestProcessJobId: String? = jobManger.find { it.factoryKey == PushProcessMessageJobV2.KEY } .maxByOrNull { it.createTime } ?.id diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageErrorV2Job.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageErrorV2Job.kt new file mode 100644 index 0000000000..e0cc77bde1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageErrorV2Job.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobs + +import android.content.Context +import androidx.annotation.WorkerThread +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.JsonJobData +import org.thoughtcrime.securesms.jobmanager.impl.ChangeNumberConstraint +import org.thoughtcrime.securesms.messages.ExceptionMetadata +import org.thoughtcrime.securesms.messages.MessageContentProcessorV2 +import org.thoughtcrime.securesms.messages.MessageState +import org.thoughtcrime.securesms.recipients.Recipient + +/** + * Process messages that did not decrypt/validate successfully. + */ +class PushProcessMessageErrorV2Job private constructor( + parameters: Parameters, + private val messageState: MessageState, + private val exceptionMetadata: ExceptionMetadata, + private val timestamp: Long +) : BaseJob(parameters) { + + constructor(messageState: MessageState, exceptionMetadata: ExceptionMetadata, timestamp: Long) : this( + parameters = createParameters(exceptionMetadata), + messageState = messageState, + exceptionMetadata = exceptionMetadata, + timestamp = timestamp + ) + + override fun getFactoryKey(): String = KEY + + override fun shouldTrace(): Boolean = true + + override fun serialize(): ByteArray? { + return JsonJobData.Builder() + .putInt(KEY_MESSAGE_STATE, messageState.ordinal) + .putLong(KEY_TIMESTAMP, timestamp) + .putString(KEY_EXCEPTION_SENDER, exceptionMetadata.sender) + .putInt(KEY_EXCEPTION_DEVICE, exceptionMetadata.senderDevice) + .putString(KEY_EXCEPTION_GROUP_ID, exceptionMetadata.groupId?.toString()) + .serialize() + } + + override fun onRun() { + if (messageState == MessageState.DECRYPTED_OK || messageState == MessageState.NOOP) { + Log.w(TAG, "Error job queued for valid or no-op decryption, generally this shouldn't happen. Bailing on state: $messageState") + return + } + + MessageContentProcessorV2.create(context).processException(messageState, exceptionMetadata, timestamp) + } + + override fun onShouldRetry(e: Exception): Boolean = false + + override fun onFailure() = Unit + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): PushProcessMessageErrorV2Job { + val data = JsonJobData.deserialize(serializedData) + + val state = MessageState.values()[data.getInt(KEY_MESSAGE_STATE)] + check(state != MessageState.DECRYPTED_OK && state != MessageState.NOOP) + + val exceptionMetadata = ExceptionMetadata( + sender = data.getString(KEY_EXCEPTION_SENDER), + senderDevice = data.getInt(KEY_EXCEPTION_DEVICE), + groupId = GroupId.parseNullableOrThrow(data.getStringOrDefault(KEY_EXCEPTION_GROUP_ID, null)) + ) + + return PushProcessMessageErrorV2Job(parameters, state, exceptionMetadata, data.getLong(KEY_TIMESTAMP)) + } + } + + companion object { + const val KEY = "PushProcessMessageErrorV2Job" + + val TAG = Log.tag(PushProcessMessageErrorV2Job::class.java) + + private const val KEY_MESSAGE_STATE = "message_state" + private const val KEY_TIMESTAMP = "timestamp" + private const val KEY_EXCEPTION_SENDER = "exception_sender" + private const val KEY_EXCEPTION_DEVICE = "exception_device" + private const val KEY_EXCEPTION_GROUP_ID = "exception_groupId" + + @WorkerThread + private fun createParameters(exceptionMetadata: ExceptionMetadata): Parameters { + val context: Context = ApplicationDependencies.getApplication() + + val recipient = exceptionMetadata.groupId?.let { Recipient.externalPossiblyMigratedGroup(it) } ?: Recipient.external(context, exceptionMetadata.sender) + + return Parameters.Builder() + .setMaxAttempts(Parameters.UNLIMITED) + .addConstraint(ChangeNumberConstraint.KEY) + .setQueue(PushProcessMessageJobV2.getQueueName(recipient.id)) + .build() + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java deleted file mode 100644 index da20f9676f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ /dev/null @@ -1,212 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.groups.GroupChangeBusyException; -import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.groups.GroupsV1MigratedCache; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.ChangeNumberConstraint; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.messages.MessageContentProcessor; -import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata; -import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.util.Base64; -import org.thoughtcrime.securesms.util.GroupUtil; -import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException; -import org.whispersystems.signalservice.api.messages.SignalServiceContent; -import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; - -import java.io.IOException; -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -public final class PushProcessMessageJob extends BaseJob { - - public static final String KEY = "PushProcessJob"; - public static final String QUEUE_PREFIX = "__PUSH_PROCESS_JOB__"; - - public static final String TAG = Log.tag(PushProcessMessageJob.class); - - private static final String KEY_MESSAGE_STATE = "message_state"; - private static final String KEY_MESSAGE_PLAINTEXT = "message_content"; - private static final String KEY_SMS_MESSAGE_ID = "sms_message_id"; - private static final String KEY_TIMESTAMP = "timestamp"; - private static final String KEY_EXCEPTION_SENDER = "exception_sender"; - private static final String KEY_EXCEPTION_DEVICE = "exception_device"; - private static final String KEY_EXCEPTION_GROUP_ID = "exception_groupId"; - - @NonNull private final MessageState messageState; - @Nullable private final SignalServiceContent content; - @Nullable private final ExceptionMetadata exceptionMetadata; - private final long smsMessageId; - private final long timestamp; - - @WorkerThread - public PushProcessMessageJob(@NonNull MessageState messageState, - @Nullable SignalServiceContent content, - @Nullable ExceptionMetadata exceptionMetadata, - long smsMessageId, - long timestamp) - { - this(createParameters(content, exceptionMetadata), - messageState, - content, - exceptionMetadata, - smsMessageId, - timestamp); - } - - private PushProcessMessageJob(@NonNull Parameters parameters, - @NonNull MessageState messageState, - @Nullable SignalServiceContent content, - @Nullable ExceptionMetadata exceptionMetadata, - long smsMessageId, - long timestamp) - { - super(parameters); - - this.messageState = messageState; - this.exceptionMetadata = exceptionMetadata; - this.content = content; - this.smsMessageId = smsMessageId; - this.timestamp = timestamp; - } - - public static @NonNull String getQueueName(@NonNull RecipientId recipientId) { - return QUEUE_PREFIX + recipientId.toQueueKey(); - } - - @WorkerThread - private static @NonNull Parameters createParameters(@Nullable SignalServiceContent content, @Nullable ExceptionMetadata exceptionMetadata) { - Context context = ApplicationDependencies.getApplication(); - String queueName = QUEUE_PREFIX; - Parameters.Builder builder = new Parameters.Builder() - .setMaxAttempts(Parameters.UNLIMITED) - .addConstraint(ChangeNumberConstraint.KEY); - - if (content != null) { - SignalServiceGroupV2 signalServiceGroupContext = GroupUtil.getGroupContextIfPresent(content); - - if (signalServiceGroupContext != null) { - GroupId groupId = GroupId.v2(signalServiceGroupContext.getMasterKey()); - - queueName = getQueueName(Recipient.externalPossiblyMigratedGroup(groupId).getId()); - - if (groupId.isV2()) { - int localRevision = SignalDatabase.groups().getGroupV2Revision(groupId.requireV2()); - - if (signalServiceGroupContext.getRevision() > localRevision || - GroupsV1MigratedCache.hasV1Group(groupId.requireV2())) - { - Log.i(TAG, "Adding network constraint to group-related job."); - builder.addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(30)); - } - } - } else if (content.getSyncMessage().isPresent() && content.getSyncMessage().get().getSent().isPresent() && content.getSyncMessage().get().getSent().get().getDestination().isPresent()) { - queueName = getQueueName(RecipientId.from(content.getSyncMessage().get().getSent().get().getDestination().get())); - } else { - queueName = getQueueName(RecipientId.from(content.getSender())); - } - } else if (exceptionMetadata != null) { - Recipient recipient = exceptionMetadata.getGroupId() != null ? Recipient.externalPossiblyMigratedGroup(exceptionMetadata.getGroupId()) - : Recipient.external(context, exceptionMetadata.getSender()); - queueName = getQueueName(recipient.getId()); - } - - builder.setQueue(queueName); - - return builder.build(); - } - - @Override - protected boolean shouldTrace() { - return true; - } - - @Override - public @Nullable byte[] serialize() { - JsonJobData.Builder dataBuilder = new JsonJobData.Builder() - .putInt(KEY_MESSAGE_STATE, messageState.ordinal()) - .putLong(KEY_SMS_MESSAGE_ID, smsMessageId) - .putLong(KEY_TIMESTAMP, timestamp); - - if (messageState == MessageState.DECRYPTED_OK) { - dataBuilder.putString(KEY_MESSAGE_PLAINTEXT, Base64.encodeBytes(Objects.requireNonNull(content).serialize())); - } else { - Objects.requireNonNull(exceptionMetadata); - dataBuilder.putString(KEY_EXCEPTION_SENDER, exceptionMetadata.getSender()) - .putInt(KEY_EXCEPTION_DEVICE, exceptionMetadata.getSenderDevice()) - .putString(KEY_EXCEPTION_GROUP_ID, exceptionMetadata.getGroupId() == null ? null : exceptionMetadata.getGroupId().toString()); - } - - return dataBuilder.serialize(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws Exception { - MessageContentProcessor processor = MessageContentProcessor.create(context); - processor.process(messageState, content, exceptionMetadata, timestamp, smsMessageId); - } - - @Override - public boolean onShouldRetry(@NonNull Exception e) { - return e instanceof PushNetworkException || - e instanceof NoCredentialForRedemptionTimeException || - e instanceof GroupChangeBusyException; - } - - @Override - public void onFailure() { - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull PushProcessMessageJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { - JsonJobData data = JsonJobData.deserialize(serializedData); - - try { - MessageState state = MessageState.values()[data.getInt(KEY_MESSAGE_STATE)]; - - if (state == MessageState.DECRYPTED_OK) { - return new PushProcessMessageJob(parameters, - state, - SignalServiceContent.deserialize(Base64.decode(data.getString(KEY_MESSAGE_PLAINTEXT))), - null, - data.getLong(KEY_SMS_MESSAGE_ID), - data.getLong(KEY_TIMESTAMP)); - } else { - ExceptionMetadata exceptionMetadata = new ExceptionMetadata(data.getString(KEY_EXCEPTION_SENDER), - data.getInt(KEY_EXCEPTION_DEVICE), - GroupId.parseNullableOrThrow(data.getStringOrDefault(KEY_EXCEPTION_GROUP_ID, null))); - - return new PushProcessMessageJob(parameters, - state, - null, - exceptionMetadata, - data.getLong(KEY_SMS_MESSAGE_ID), - data.getLong(KEY_TIMESTAMP)); - } - } catch (IOException e) { - throw new AssertionError(e); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJobV2.kt index 777efd3698..11fd8641e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJobV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJobV2.kt @@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.crypto.protos.CompleteMessage import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException +import org.whispersystems.signalservice.internal.push.SignalServiceProtos import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope import java.io.IOException @@ -29,7 +30,7 @@ import org.whispersystems.signalservice.api.crypto.protos.EnvelopeMetadata as En class PushProcessMessageJobV2 private constructor( parameters: Parameters, - private val envelope: Envelope, + private val envelope: SignalServiceProtos.Envelope, private val content: Content, private val metadata: EnvelopeMetadata, private val serverDeliveredTimestamp: Long @@ -108,7 +109,8 @@ class PushProcessMessageJobV2 private constructor( */ private val empty1to1QueueCache = HashSet() - private fun getQueueName(recipientId: RecipientId): String { + @JvmStatic + fun getQueueName(recipientId: RecipientId): String { return QUEUE_PREFIX + recipientId.toQueueKey() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java index 7b85123901..f37d75ad21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java @@ -41,7 +41,7 @@ final class RequestGroupV2InfoWorkerJob extends BaseJob { @WorkerThread RequestGroupV2InfoWorkerJob(@NonNull GroupId.V2 groupId, int toRevision) { this(new Parameters.Builder() - .setQueue(PushProcessMessageJob.getQueueName(Recipient.externalGroupExact(groupId).getId())) + .setQueue(PushProcessMessageJobV2.getQueueName(Recipient.externalGroupExact(groupId).getId())) .addConstraint(NetworkConstraint.KEY) .setLifespan(TimeUnit.DAYS.toMillis(1)) .setMaxAttempts(Parameters.UNLIMITED) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt index 0aadf21701..a4bc752f22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt @@ -48,14 +48,13 @@ import org.thoughtcrime.securesms.jobs.PaymentLedgerUpdateJob import org.thoughtcrime.securesms.jobs.PaymentTransactionCheckJob import org.thoughtcrime.securesms.jobs.ProfileKeySendJob import org.thoughtcrime.securesms.jobs.PushProcessEarlyMessagesJob -import org.thoughtcrime.securesms.jobs.PushProcessMessageJob +import org.thoughtcrime.securesms.jobs.PushProcessMessageJobV2 import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.jobs.RetrieveProfileJob import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob import org.thoughtcrime.securesms.jobs.TrimThreadJob import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil -import org.thoughtcrime.securesms.messages.MessageContentProcessor.StorageFailedException import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.debug import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.log import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.warn @@ -613,7 +612,7 @@ object DataMessageProcessor { val paymentNotification = message.payment.notification val uuid = UUID.randomUUID() - val queue = "Payment_" + PushProcessMessageJob.getQueueName(senderRecipientId) + val queue = "Payment_" + PushProcessMessageJobV2.getQueueName(senderRecipientId) try { SignalDatabase.payments.createIncomingPayment( diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/EditMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/EditMessageProcessor.kt index ed6881307f..04c171f9e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/EditMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/EditMessageProcessor.kt @@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.util.hasSharedContact import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata import org.whispersystems.signalservice.internal.push.SignalServiceProtos import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope import java.util.Optional object EditMessageProcessor { @@ -41,7 +42,7 @@ object EditMessageProcessor { context: Context, senderRecipient: Recipient, threadRecipient: Recipient, - envelope: SignalServiceProtos.Envelope, + envelope: Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, earlyMessageCacheEntry: EarlyMessageCacheEntry? @@ -116,7 +117,7 @@ object EditMessageProcessor { private fun handleEditMediaMessage( senderRecipientId: RecipientId, groupId: GroupId.V2?, - envelope: SignalServiceProtos.Envelope, + envelope: Envelope, metadata: EnvelopeMetadata, message: DataMessage, targetMessage: MediaMmsMessageRecord @@ -176,7 +177,7 @@ object EditMessageProcessor { private fun handleEditTextMessage( senderRecipientId: RecipientId, groupId: GroupId.V2?, - envelope: SignalServiceProtos.Envelope, + envelope: Envelope, metadata: EnvelopeMetadata, message: DataMessage, targetMessage: MediaMmsMessageRecord diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/ExceptionMetadata.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/ExceptionMetadata.kt new file mode 100644 index 0000000000..31ef7161e9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/ExceptionMetadata.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.thoughtcrime.securesms.messages + +import org.thoughtcrime.securesms.groups.GroupId + +/** + * Message processing exception metadata. + */ +class ExceptionMetadata @JvmOverloads constructor(val sender: String, val senderDevice: Int, val groupId: GroupId? = null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt index b1f2a7759a..f82cec357b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.messages -import android.annotation.SuppressLint import android.app.Application import android.app.Service import android.content.Context @@ -17,15 +16,11 @@ import org.thoughtcrime.securesms.crypto.ReentrantSessionLock import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.groups.GroupsV2ProcessingLock -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobmanager.JobTracker -import org.thoughtcrime.securesms.jobmanager.JobTracker.JobListener import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil.startWhenCapable -import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob -import org.thoughtcrime.securesms.jobs.PushProcessMessageJob +import org.thoughtcrime.securesms.jobs.PushProcessMessageErrorV2Job import org.thoughtcrime.securesms.jobs.PushProcessMessageJobV2 import org.thoughtcrime.securesms.jobs.UnableToStartException import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -161,19 +156,6 @@ class IncomingMessageObserver(private val context: Application) { decryptionDrainedListeners.remove(listener) } - fun notifyDecryptionsDrained() { - if (ApplicationDependencies.getJobManager().isQueueEmpty(PushDecryptMessageJob.QUEUE)) { - Log.i(TAG, "Queue was empty when notified. Signaling change.") - connectionNecessarySemaphore.release() - } else { - Log.i(TAG, "Queue still had items when notified. Registering listener to signal change.") - ApplicationDependencies.getJobManager().addListener( - { it.parameters.queue == PushDecryptMessageJob.QUEUE }, - DecryptionDrainedQueueListener() - ) - } - } - private fun onAppForegrounded() { lock.withLock { appVisible = true @@ -213,17 +195,15 @@ class IncomingMessageObserver(private val context: Application) { val hasNetwork = NetworkConstraint.isMet(context) val hasProxy = SignalStore.proxy().isProxyEnabled val forceWebsocket = SignalStore.internalValues().isWebsocketModeForced - val decryptQueueEmpty = ApplicationDependencies.getJobManager().isQueueEmpty(PushDecryptMessageJob.QUEUE) val lastInteractionString = if (appVisibleSnapshot) "N/A" else timeIdle.toString() + " ms (" + (if (timeIdle < maxBackgroundTime) "within limit" else "over limit") + ")" val conclusion = registered && (appVisibleSnapshot || timeIdle < maxBackgroundTime || !fcmEnabled || keepAliveEntries.isNotEmpty()) && - hasNetwork && - decryptQueueEmpty + hasNetwork val needsConnectionString = if (conclusion) "Needs Connection" else "Does Not Need Connection" - Log.d(TAG, "[$needsConnectionString] Network: $hasNetwork, Foreground: $appVisibleSnapshot, Time Since Last Interaction: $lastInteractionString, FCM: $fcmEnabled, Stay open requests: $keepAliveEntries, Registered: $registered, Proxy: $hasProxy, Force websocket: $forceWebsocket, Decrypt Queue Empty: $decryptQueueEmpty") + Log.d(TAG, "[$needsConnectionString] Network: $hasNetwork, Foreground: $appVisibleSnapshot, Time Since Last Interaction: $lastInteractionString, FCM: $fcmEnabled, Stay open requests: $keepAliveEntries, Registered: $registered, Proxy: $hasProxy, Force websocket: $forceWebsocket") return conclusion } @@ -309,11 +289,9 @@ class IncomingMessageObserver(private val context: Application) { } is MessageDecryptor.Result.Error -> { return result.followUpOperations + FollowUpOperation { - PushProcessMessageJob( + PushProcessMessageErrorV2Job( result.toMessageState(), - null, result.errorMetadata.toExceptionMetadata(), - -1, result.envelope.timestamp ) } @@ -342,19 +320,19 @@ class IncomingMessageObserver(private val context: Application) { SignalDatabase.messageLog.deleteEntryForRecipient(envelope.timestamp, senderId, envelope.sourceDevice) } - private fun MessageDecryptor.Result.toMessageState(): MessageContentProcessor.MessageState { + private fun MessageDecryptor.Result.toMessageState(): MessageState { return when (this) { - is MessageDecryptor.Result.DecryptionError -> MessageContentProcessor.MessageState.DECRYPTION_ERROR - is MessageDecryptor.Result.Ignore -> MessageContentProcessor.MessageState.NOOP - is MessageDecryptor.Result.InvalidVersion -> MessageContentProcessor.MessageState.INVALID_VERSION - is MessageDecryptor.Result.LegacyMessage -> MessageContentProcessor.MessageState.LEGACY_MESSAGE - is MessageDecryptor.Result.Success -> MessageContentProcessor.MessageState.DECRYPTED_OK - is MessageDecryptor.Result.UnsupportedDataMessage -> MessageContentProcessor.MessageState.UNSUPPORTED_DATA_MESSAGE + is MessageDecryptor.Result.DecryptionError -> MessageState.DECRYPTION_ERROR + is MessageDecryptor.Result.Ignore -> MessageState.NOOP + is MessageDecryptor.Result.InvalidVersion -> MessageState.INVALID_VERSION + is MessageDecryptor.Result.LegacyMessage -> MessageState.LEGACY_MESSAGE + is MessageDecryptor.Result.Success -> MessageState.DECRYPTED_OK + is MessageDecryptor.Result.UnsupportedDataMessage -> MessageState.UNSUPPORTED_DATA_MESSAGE } } - private fun MessageDecryptor.ErrorMetadata.toExceptionMetadata(): MessageContentProcessor.ExceptionMetadata { - return MessageContentProcessor.ExceptionMetadata( + private fun MessageDecryptor.ErrorMetadata.toExceptionMetadata(): ExceptionMetadata { + return ExceptionMetadata( this.sender, this.senderDevice, this.groupId @@ -463,21 +441,6 @@ class IncomingMessageObserver(private val context: Application) { } } - private inner class DecryptionDrainedQueueListener : JobListener { - @SuppressLint("WrongThread") - override fun onStateChanged(job: Job, jobState: JobTracker.JobState) { - if (jobState.isComplete) { - if (ApplicationDependencies.getJobManager().isQueueEmpty(PushDecryptMessageJob.QUEUE)) { - Log.i(TAG, "Queue is now empty. Signaling change.") - connectionNecessarySemaphore.release() - ApplicationDependencies.getJobManager().removeListener(this) - } else { - Log.i(TAG, "Item finished in queue, but it's still not empty. Waiting to signal change.") - } - } - } - } - class ForegroundService : Service() { override fun onBind(intent: Intent?): IBinder? { return null diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java deleted file mode 100644 index bd717fcccf..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ /dev/null @@ -1,3536 +0,0 @@ -package org.thoughtcrime.securesms.messages; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Color; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import com.annimon.stream.Collectors; -import com.annimon.stream.Stream; -import com.google.protobuf.ByteString; -import com.mobilecoin.lib.exceptions.SerializationException; - -import org.signal.core.util.Hex; -import org.signal.core.util.concurrent.SignalExecutors; -import org.signal.core.util.logging.Log; -import org.signal.libsignal.protocol.SignalProtocolAddress; -import org.signal.libsignal.protocol.ecc.ECPublicKey; -import org.signal.libsignal.protocol.message.DecryptionErrorMessage; -import org.signal.libsignal.protocol.state.SessionRecord; -import org.signal.libsignal.zkgroup.profiles.ProfileKey; -import org.signal.ringrtc.CallId; -import org.thoughtcrime.securesms.attachments.Attachment; -import org.thoughtcrime.securesms.attachments.DatabaseAttachment; -import org.thoughtcrime.securesms.attachments.PointerAttachment; -import org.thoughtcrime.securesms.attachments.TombstoneAttachment; -import org.thoughtcrime.securesms.attachments.UriAttachment; -import org.thoughtcrime.securesms.components.emoji.EmojiUtil; -import org.thoughtcrime.securesms.contactshare.Contact; -import org.thoughtcrime.securesms.contactshare.ContactModelMapper; -import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; -import org.thoughtcrime.securesms.crypto.SecurityEvent; -import org.thoughtcrime.securesms.database.AttachmentTable; -import org.thoughtcrime.securesms.database.CallTable; -import org.thoughtcrime.securesms.database.GroupReceiptTable; -import org.thoughtcrime.securesms.database.GroupReceiptTable.GroupReceiptInfo; -import org.thoughtcrime.securesms.database.GroupTable; -import org.thoughtcrime.securesms.database.MessageTable; -import org.thoughtcrime.securesms.database.MessageTable.InsertResult; -import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; -import org.thoughtcrime.securesms.database.NoSuchMessageException; -import org.thoughtcrime.securesms.database.PaymentMetaDataUtil; -import org.thoughtcrime.securesms.database.PaymentTable; -import org.thoughtcrime.securesms.database.RecipientTable; -import org.thoughtcrime.securesms.database.SentStorySyncManifest; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.StickerTable; -import org.thoughtcrime.securesms.database.ThreadTable; -import org.thoughtcrime.securesms.database.model.DatabaseProtosUtil; -import org.thoughtcrime.securesms.database.model.DistributionListId; -import org.thoughtcrime.securesms.database.model.GroupRecord; -import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; -import org.thoughtcrime.securesms.database.model.Mention; -import org.thoughtcrime.securesms.database.model.MessageId; -import org.thoughtcrime.securesms.database.model.MessageLogEntry; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.thoughtcrime.securesms.database.model.ParentStoryId; -import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel; -import org.thoughtcrime.securesms.database.model.ReactionRecord; -import org.thoughtcrime.securesms.database.model.StickerRecord; -import org.thoughtcrime.securesms.database.model.StoryType; -import org.thoughtcrime.securesms.database.model.ThreadRecord; -import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; -import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor; -import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge; -import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.groups.BadGroupIdException; -import org.thoughtcrime.securesms.groups.GroupChangeBusyException; -import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.groups.GroupManager; -import org.thoughtcrime.securesms.groups.GroupNotAMemberException; -import org.thoughtcrime.securesms.groups.GroupsV1MigratedCache; -import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; -import org.thoughtcrime.securesms.jobs.AutomaticSessionResetJob; -import org.thoughtcrime.securesms.jobs.GroupCallPeekJob; -import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceContactSyncJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackSyncJob; -import org.thoughtcrime.securesms.jobs.NullMessageSendJob; -import org.thoughtcrime.securesms.jobs.PaymentLedgerUpdateJob; -import org.thoughtcrime.securesms.jobs.PaymentTransactionCheckJob; -import org.thoughtcrime.securesms.jobs.ProfileKeySendJob; -import org.thoughtcrime.securesms.jobs.PushProcessEarlyMessagesJob; -import org.thoughtcrime.securesms.jobs.PushProcessMessageJob; -import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; -import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob; -import org.thoughtcrime.securesms.jobs.ResendMessageJob; -import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; -import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob; -import org.thoughtcrime.securesms.jobs.SenderKeyDistributionSendJob; -import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob; -import org.thoughtcrime.securesms.jobs.TrimThreadJob; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.linkpreview.LinkPreview; -import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; -import org.thoughtcrime.securesms.mms.IncomingMediaMessage; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.OutgoingMessage; -import org.thoughtcrime.securesms.mms.QuoteModel; -import org.thoughtcrime.securesms.mms.SlideDeck; -import org.thoughtcrime.securesms.mms.StickerSlide; -import org.thoughtcrime.securesms.notifications.MarkReadReceiver; -import org.thoughtcrime.securesms.notifications.MessageNotifier; -import org.thoughtcrime.securesms.notifications.v2.ConversationId; -import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress; -import org.thoughtcrime.securesms.ratelimit.RateLimitUtil; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.recipients.RecipientUtil; -import org.thoughtcrime.securesms.ringrtc.RemotePeer; -import org.thoughtcrime.securesms.service.webrtc.WebRtcData; -import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId; -import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; -import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; -import org.thoughtcrime.securesms.sms.IncomingTextMessage; -import org.thoughtcrime.securesms.stickers.StickerLocator; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; -import org.thoughtcrime.securesms.stories.Stories; -import org.thoughtcrime.securesms.util.Base64; -import org.thoughtcrime.securesms.util.FeatureFlags; -import org.thoughtcrime.securesms.util.GroupUtil; -import org.thoughtcrime.securesms.util.IdentityUtil; -import org.thoughtcrime.securesms.util.LinkUtil; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.MessageConstraintsUtil; -import org.thoughtcrime.securesms.util.MessageRecordUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; -import org.whispersystems.signalservice.api.messages.SignalServiceContent; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; -import org.whispersystems.signalservice.api.messages.SignalServicePreview; -import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; -import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; -import org.whispersystems.signalservice.api.messages.calls.BusyMessage; -import org.whispersystems.signalservice.api.messages.calls.HangupMessage; -import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; -import org.whispersystems.signalservice.api.messages.calls.OfferMessage; -import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage; -import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; -import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; -import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage; -import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; -import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage; -import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage; -import org.whispersystems.signalservice.api.messages.multidevice.OutgoingPaymentMessage; -import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; -import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; -import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; -import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; -import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage; -import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage; -import org.whispersystems.signalservice.api.messages.shared.SharedContact; -import org.whispersystems.signalservice.api.payments.Money; -import org.whispersystems.signalservice.api.push.DistributionId; -import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage; - -import java.io.IOException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -/** - * Takes data about a decrypted message, transforms it into user-presentable data, and writes that - * data to our data stores. - */ -@SuppressWarnings({ "OptionalGetWithoutIsPresent", "OptionalIsPresent" }) -public class MessageContentProcessor { - - public static final String TAG = Log.tag(MessageContentProcessor.class); - - private final Context context; - - public static MessageContentProcessor create(@NonNull Context context) { - return new MessageContentProcessor(context); - } - - @VisibleForTesting - MessageContentProcessor(@NonNull Context context) { - this.context = context; - } - - /** - * Given the details about a message decryption, this will insert the proper message content into - * the database. - * - * This is super-stateful, and it's recommended that this be run in a transaction so that no - * intermediate results are persisted to the database if the app were to crash. - */ - public void process(MessageState messageState, @Nullable SignalServiceContent content, @Nullable ExceptionMetadata exceptionMetadata, long envelopeTimestamp, long smsMessageId) - throws IOException, GroupChangeBusyException - { - process(messageState, content, exceptionMetadata, envelopeTimestamp, smsMessageId, false); - } - - /** - * The same as {@link #process(MessageState, SignalServiceContent, ExceptionMetadata, long, long)}, except specifically targeted at early content. - * Using this method will *not* store or enqueue early content jobs if we detect this as being early, to avoid recursive scenarios. - */ - public void processEarlyContent(MessageState messageState, @Nullable SignalServiceContent content, @Nullable ExceptionMetadata exceptionMetadata, long envelopeTimestamp, long smsMessageId) - throws IOException, GroupChangeBusyException - { - process(messageState, content, exceptionMetadata, envelopeTimestamp, smsMessageId, true); - } - - - private void process(MessageState messageState, @Nullable SignalServiceContent content, @Nullable ExceptionMetadata exceptionMetadata, long envelopeTimestamp, long smsMessageId, boolean processingEarlyContent) - throws IOException, GroupChangeBusyException - { - Optional optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.empty(); - - if (messageState == MessageState.DECRYPTED_OK) { - - if (content != null) { - Recipient senderRecipient = Recipient.externalPush(content.getSender()); - - handleMessage(content, senderRecipient, optionalSmsMessageId, processingEarlyContent); - - Optional> earlyContent = ApplicationDependencies.getEarlyMessageCache() - .retrieve(senderRecipient.getId(), content.getTimestamp()); - if (!processingEarlyContent && earlyContent.isPresent()) { - log(String.valueOf(content.getTimestamp()), "Found " + earlyContent.get().size() + " dependent item(s) that were retrieved earlier. Processing."); - - for (SignalServiceContent earlyItem : earlyContent.get()) { - handleMessage(earlyItem, senderRecipient, Optional.empty(), true); - } - } - } else { - warn("null", "Null content. Ignoring message."); - } - } else if (exceptionMetadata != null) { - handleExceptionMessage(messageState, exceptionMetadata, envelopeTimestamp, optionalSmsMessageId); - } else if (messageState == MessageState.NOOP) { - Log.d(TAG, "Nothing to do: " + messageState.name()); - } else { - warn("Bad state! messageState: " + messageState); - } - } - - private void handleMessage(@NonNull SignalServiceContent content, @NonNull Recipient senderRecipient, @NonNull Optional smsMessageId, boolean processingEarlyContent) - throws IOException, GroupChangeBusyException - { - try { - Recipient threadRecipient = getMessageDestination(content, senderRecipient); - - if (shouldIgnore(content, senderRecipient, threadRecipient)) { - log(content.getTimestamp(), "Ignoring message."); - return; - } - - PendingRetryReceiptModel pending = ApplicationDependencies.getPendingRetryReceiptCache().get(senderRecipient.getId(), content.getTimestamp()); - long receivedTime = handlePendingRetry(pending, content, threadRecipient); - - log(String.valueOf(content.getTimestamp()), "Beginning message processing. Sender: " + formatSender(senderRecipient, content)); - - if (content.getDataMessage().isPresent()) { - GroupTable groupDatabase = SignalDatabase.groups(); - SignalServiceDataMessage message = content.getDataMessage().get(); - boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent() || message.getMentions().isPresent() || message.getBodyRanges().isPresent(); - Optional groupId = GroupUtil.idFromGroupContext(message.getGroupContext()); - boolean isGv2Message = groupId.isPresent() && groupId.get().isV2(); - - if (isGv2Message) { - if (handleGv2PreProcessing(groupId.orElse(null).requireV2(), content, content.getDataMessage().get().getGroupContext().get(), senderRecipient)) { - return; - } - } - - MessageId messageId = null; - - if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId); - else if (message.isEndSession()) messageId = handleEndSessionMessage(content, smsMessageId, senderRecipient); - else if (message.isExpirationUpdate()) messageId = handleExpirationUpdate(content, message, smsMessageId, groupId, senderRecipient, threadRecipient, receivedTime, false); - else if (message.getReaction().isPresent() && message.getStoryContext().isPresent()) messageId = handleStoryReaction(content, message, senderRecipient); - else if (message.getReaction().isPresent()) messageId = handleReaction(content, message, senderRecipient, processingEarlyContent); - else if (message.getRemoteDelete().isPresent()) messageId = handleRemoteDelete(content, message, senderRecipient, processingEarlyContent); - else if (message.isActivatePaymentsRequest()) messageId = handlePaymentActivation(content, message, smsMessageId, senderRecipient, receivedTime, true, false); - else if (message.isPaymentsActivated()) messageId = handlePaymentActivation(content, message, smsMessageId, senderRecipient, receivedTime, false, true); - else if (message.getPayment().isPresent()) messageId = handlePayment(content, message, smsMessageId, senderRecipient, receivedTime); - else if (message.getStoryContext().isPresent()) messageId = handleStoryReply(content, message, senderRecipient, receivedTime); - else if (message.getGiftBadge().isPresent()) messageId = handleGiftMessage(content, message, senderRecipient, threadRecipient, receivedTime); - else if (isMediaMessage) messageId = handleMediaMessage(content, message, smsMessageId, senderRecipient, threadRecipient, receivedTime); - else if (message.getBody().isPresent()) messageId = handleTextMessage(content, message, smsMessageId, groupId, senderRecipient, threadRecipient, receivedTime); - else if (message.getGroupCallUpdate().isPresent()) handleGroupCallUpdateMessage(content, message, groupId, senderRecipient); - - if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { - handleUnknownGroupMessage(content, message.getGroupContext().get(), senderRecipient); - } - - if (message.getProfileKey().isPresent()) { - handleProfileKey(content, message.getProfileKey().get(), senderRecipient); - } - - if (content.isNeedsReceipt() && messageId != null) { - handleNeedsDeliveryReceipt(senderRecipient.getId(), message, messageId); - } else if (!content.isNeedsReceipt()) { - if (RecipientUtil.shouldHaveProfileKey(threadRecipient)) { - Log.w(TAG, "Received an unsealed sender message from " + senderRecipient.getId() + ", but they should already have our profile key. Correcting."); - - if (groupId.isPresent() && groupId.get().isV2()) { - Log.i(TAG, "Message was to a GV2 group. Ensuring our group profile keys are up to date."); - ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob(false)) - .then(GroupV2UpdateSelfProfileKeyJob.withQueueLimits(groupId.get().requireV2())) - .enqueue(); - } else if (!threadRecipient.isGroup()) { - Log.i(TAG, "Message was to a 1:1. Ensuring this user has our profile key."); - ProfileKeySendJob profileSendJob = ProfileKeySendJob.create(SignalDatabase.threads().getOrCreateThreadIdFor(threadRecipient), true); - - if (profileSendJob != null) { - ApplicationDependencies.getJobManager() - .startChain(new RefreshAttributesJob(false)) - .then(profileSendJob) - .enqueue(); - } - } - } - } - } else if (content.getSyncMessage().isPresent()) { - TextSecurePreferences.setMultiDevice(context, true); - - SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); - - if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(content, syncMessage.getSent().get(), senderRecipient, processingEarlyContent); - else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(syncMessage.getRequest().get(), content.getTimestamp()); - else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(content, syncMessage.getRead().get(), content.getTimestamp(), processingEarlyContent); - else if (syncMessage.getViewed().isPresent()) handleSynchronizeViewedMessage(syncMessage.getViewed().get(), content.getTimestamp()); - else if (syncMessage.getViewOnceOpen().isPresent()) handleSynchronizeViewOnceOpenMessage(content, syncMessage.getViewOnceOpen().get(), content.getTimestamp(), processingEarlyContent); - else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get()); - else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().get(), content.getTimestamp()); - else if (syncMessage.getConfiguration().isPresent()) handleSynchronizeConfigurationMessage(syncMessage.getConfiguration().get(), content.getTimestamp()); - else if (syncMessage.getBlockedList().isPresent()) handleSynchronizeBlockedListMessage(syncMessage.getBlockedList().get()); - else if (syncMessage.getFetchType().isPresent()) handleSynchronizeFetchMessage(syncMessage.getFetchType().get(), content.getTimestamp()); - else if (syncMessage.getMessageRequestResponse().isPresent()) handleSynchronizeMessageRequestResponse(syncMessage.getMessageRequestResponse().get(), content.getTimestamp()); - else if (syncMessage.getOutgoingPaymentMessage().isPresent()) handleSynchronizeOutgoingPayment(content, syncMessage.getOutgoingPaymentMessage().get()); - else if (syncMessage.getKeys().isPresent()) handleSynchronizeKeys(syncMessage.getKeys().get(), content.getTimestamp()); - else if (syncMessage.getContacts().isPresent()) handleSynchronizeContacts(syncMessage.getContacts().get(), content.getTimestamp()); - else if (syncMessage.getCallEvent().isPresent()) { - SyncMessage.CallEvent.Type type = syncMessage.getCallEvent().get().getType(); - if (type == SyncMessage.CallEvent.Type.GROUP_CALL || type == SyncMessage.CallEvent.Type.AD_HOC_CALL) { - handleSynchronizeGroupOrAdHocCallEvent(syncMessage.getCallEvent().get(), content.getTimestamp()); - } else { - handleSynchronizeCallEvent(syncMessage.getCallEvent().get(), content.getTimestamp()); - } - } - else warn(String.valueOf(content.getTimestamp()), "Contains no known sync types..."); - } else if (content.getCallMessage().isPresent()) { - log(String.valueOf(content.getTimestamp()), "Got call message..."); - - SignalServiceCallMessage message = content.getCallMessage().get(); - Optional destinationDeviceId = message.getDestinationDeviceId(); - - if (destinationDeviceId.isPresent() && destinationDeviceId.get() != SignalStore.account().getDeviceId()) { - log(String.valueOf(content.getTimestamp()), String.format(Locale.US, "Ignoring call message that is not for this device! intended: %d, this: %d", destinationDeviceId.get(), SignalStore.account().getDeviceId())); - return; - } - - if (message.getOfferMessage().isPresent()) handleCallOfferMessage(content, message.getOfferMessage().get(), smsMessageId, senderRecipient); - else if (message.getAnswerMessage().isPresent()) handleCallAnswerMessage(content, message.getAnswerMessage().get(), senderRecipient); - else if (message.getIceUpdateMessages().isPresent()) handleCallIceUpdateMessage(content, message.getIceUpdateMessages().get(), senderRecipient); - else if (message.getHangupMessage().isPresent()) handleCallHangupMessage(content, message.getHangupMessage().get(), smsMessageId, senderRecipient); - else if (message.getBusyMessage().isPresent()) handleCallBusyMessage(content, message.getBusyMessage().get(), senderRecipient); - else if (message.getOpaqueMessage().isPresent()) handleCallOpaqueMessage(content, message.getOpaqueMessage().get(), senderRecipient); - } else if (content.getReceiptMessage().isPresent()) { - SignalServiceReceiptMessage message = content.getReceiptMessage().get(); - - if (message.isReadReceipt()) handleReadReceipt(content, message, senderRecipient, processingEarlyContent); - else if (message.isDeliveryReceipt()) handleDeliveryReceipt(content, message, senderRecipient); - else if (message.isViewedReceipt()) handleViewedReceipt(content, message, senderRecipient, processingEarlyContent); - } else if (content.getTypingMessage().isPresent()) { - handleTypingMessage(content, content.getTypingMessage().get(), senderRecipient); - } else if (content.getStoryMessage().isPresent()) { - handleStoryMessage(content, content.getStoryMessage().get(), senderRecipient, threadRecipient); - } else if (content.getDecryptionErrorMessage().isPresent()) { - handleRetryReceipt(content, content.getDecryptionErrorMessage().get(), senderRecipient); - } else if (content.getSenderKeyDistributionMessage().isPresent()) { - // Already handled, here in order to prevent unrecognized message log - } else if (content.getPniSignatureMessage().isPresent()) { - // Already handled, here in order to prevent unrecognized message log - } else { - warn(String.valueOf(content.getTimestamp()), "Got unrecognized message!"); - } - - if (pending != null) { - warn(content.getTimestamp(), "Pending retry was processed. Deleting."); - ApplicationDependencies.getPendingRetryReceiptCache().delete(pending); - } - } catch (StorageFailedException e) { - warn(String.valueOf(content.getTimestamp()), e); - handleCorruptMessage(e.getSender(), e.getSenderDevice(), content.getTimestamp(), smsMessageId); - } catch (BadGroupIdException e) { - warn(String.valueOf(content.getTimestamp()), "Ignoring message with bad group id", e); - } - } - - private long handlePendingRetry(@Nullable PendingRetryReceiptModel pending, @NonNull SignalServiceContent content, @NonNull Recipient destination) throws BadGroupIdException { - long receivedTime = System.currentTimeMillis(); - - if (pending != null) { - warn(content.getTimestamp(), "Incoming message matches a pending retry we were expecting."); - - Long threadId = SignalDatabase.threads().getThreadIdFor(destination.getId()); - - if (threadId != null) { - ThreadTable.ConversationMetadata metadata = SignalDatabase.threads().getConversationMetadata(threadId); - long visibleThread = ApplicationDependencies.getMessageNotifier().getVisibleThread().map(ConversationId::getThreadId).orElse(-1L); - - if (threadId != visibleThread && metadata.getLastSeen() > 0 && metadata.getLastSeen() < pending.getReceivedTimestamp()) { - receivedTime = pending.getReceivedTimestamp(); - warn(content.getTimestamp(), "Thread has not been opened yet. Using received timestamp of " + receivedTime); - } else { - warn(content.getTimestamp(), "Thread was opened after receiving the original message. Using the current time for received time. (Last seen: " + metadata.getLastSeen() + ", ThreadVisible: " + (threadId == visibleThread) + ")"); - } - } else { - warn(content.getTimestamp(), "Could not find a thread for the pending message. Using current time for received time."); - } - } - - return receivedTime; - } - - private @Nullable MessageId handlePayment(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message, - @NonNull Optional smsMessageId, - @NonNull Recipient senderRecipient, - long receivedTime) - throws StorageFailedException - { - log(content.getTimestamp(), "Payment message."); - - if (!message.getPayment().isPresent()) { - throw new AssertionError(); - } - - if (!message.getPayment().get().getPaymentNotification().isPresent()) { - warn(content.getTimestamp(), "Ignoring payment message without notification"); - return null; - } - - SignalServiceDataMessage.PaymentNotification paymentNotification = message.getPayment().get().getPaymentNotification().get(); - PaymentTable paymentDatabase = SignalDatabase.payments(); - UUID uuid = UUID.randomUUID(); - String queue = "Payment_" + PushProcessMessageJob.getQueueName(senderRecipient.getId()); - MessageId messageId = null; - - try { - paymentDatabase.createIncomingPayment(uuid, - senderRecipient.getId(), - message.getTimestamp(), - paymentNotification.getNote(), - Money.MobileCoin.ZERO, - Money.MobileCoin.ZERO, - paymentNotification.getReceipt(), - true); - - IncomingMediaMessage mediaMessage = IncomingMediaMessage.createIncomingPaymentNotification(senderRecipient.getId(), - content, - receivedTime, - TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()), - uuid); - - Optional insertResult = SignalDatabase.messages().insertSecureDecryptedMessageInbox(mediaMessage, -1); - smsMessageId.ifPresent(smsId -> SignalDatabase.messages().deleteMessage(smsId)); - if (insertResult.isPresent()) { - messageId = new MessageId(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - } - } catch (PaymentTable.PublicKeyConflictException e) { - warn(content.getTimestamp(), "Ignoring payment with public key already in database"); - } catch (SerializationException e) { - warn(content.getTimestamp(), "Ignoring payment with bad data.", e); - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); - } finally { - ApplicationDependencies.getJobManager() - .startChain(new PaymentTransactionCheckJob(uuid, queue)) - .then(PaymentLedgerUpdateJob.updateLedger()) - .enqueue(); - } - - return messageId; - } - - /** - * @return True if the content should be ignored, otherwise false. - */ - private boolean handleGv2PreProcessing(@NonNull GroupId.V2 groupId, @NonNull SignalServiceContent content, @NonNull SignalServiceGroupV2 groupV2, @NonNull Recipient senderRecipient) - throws IOException, GroupChangeBusyException - { - GroupTable groupDatabase = SignalDatabase.groups(); - GroupRecord possibleGv1 = GroupsV1MigratedCache.getV1GroupByV2Id(groupId); - - if (possibleGv1 != null) { - GroupsV1MigrationUtil.performLocalMigration(context, possibleGv1.getId().requireV1()); - } - - if (!updateGv2GroupFromServerOrP2PChange(content, groupV2)) { - log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupId); - return true; - } - - Optional groupRecord = groupDatabase.getGroup(groupId); - - if (groupRecord.isPresent() && !groupRecord.get().getMembers().contains(senderRecipient.getId())) { - log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message from member not in group " + groupId + ". Sender: " + senderRecipient.getId() + " | " + senderRecipient.requireAci()); - return true; - } - - if (groupRecord.isPresent() && groupRecord.get().isAnnouncementGroup() && !groupRecord.get().getAdmins().contains(senderRecipient)) { - if (content.getDataMessage().isPresent()) { - SignalServiceDataMessage data = content.getDataMessage().get(); - if (data.getBody().isPresent() || - data.getAttachments().isPresent() || - data.getQuote().isPresent() || - data.getPreviews().isPresent() || - data.getMentions().isPresent() || - data.getSticker().isPresent()) - { - Log.w(TAG, "Ignoring message from " + senderRecipient.getId() + " because it has disallowed content, and they're not an admin in an announcement-only group."); - return true; - } - } else if (content.getTypingMessage().isPresent()) { - Log.w(TAG, "Ignoring typing indicator from " + senderRecipient.getId() + " because they're not an admin in an announcement-only group."); - return true; - } - } - - return false; - } - - /** - * Attempts to update the group to the revision mentioned in the message. - * If the local version is at least the revision in the message it will not query the server. - * If the message includes a signed change proto that is sufficient (i.e. local revision is only - * 1 revision behind), it will also not query the server in this case. - * - * @return false iff needed to query the server and was not able to because self is not a current - * member of the group. - */ - private boolean updateGv2GroupFromServerOrP2PChange(@NonNull SignalServiceContent content, - @NonNull SignalServiceGroupV2 groupV2) - throws IOException, GroupChangeBusyException - { - try { - long timestamp = groupV2.getSignedGroupChange() != null ? content.getTimestamp() : content.getTimestamp() - 1; - GroupManager.updateGroupFromServer(context, groupV2.getMasterKey(), groupV2.getRevision(), timestamp, groupV2.getSignedGroupChange()); - return true; - } catch (GroupNotAMemberException e) { - warn(String.valueOf(content.getTimestamp()), "Ignoring message for a group we're not in"); - return false; - } - } - - private void handleExceptionMessage(@NonNull MessageState messageState, @NonNull ExceptionMetadata e, long timestamp, @NonNull Optional smsMessageId) { - Recipient sender = Recipient.external(context, e.sender); - - if (sender.isBlocked()) { - warn("Ignoring exception content from blocked sender, message state:" + messageState); - return; - } - - switch (messageState) { - case DECRYPTION_ERROR: - warn(String.valueOf(timestamp), "Handling encryption error."); - SignalDatabase.messages().insertBadDecryptMessage(sender.getId(), e.senderDevice, timestamp, System.currentTimeMillis(), getThreadIdForException(e)); - break; - - case INVALID_VERSION: - warn(String.valueOf(timestamp), "Handling invalid version."); - handleInvalidVersionMessage(e.sender, e.senderDevice, timestamp, smsMessageId); - break; - - case LEGACY_MESSAGE: - warn(String.valueOf(timestamp), "Handling legacy message."); - handleLegacyMessage(e.sender, e.senderDevice, timestamp, smsMessageId); - break; - - case DUPLICATE_MESSAGE: - warn(String.valueOf(timestamp), "Duplicate message. Dropping."); - break; - - case UNSUPPORTED_DATA_MESSAGE: - warn(String.valueOf(timestamp), "Handling unsupported data message."); - handleUnsupportedDataMessage(e.sender, e.senderDevice, Optional.ofNullable(e.groupId), timestamp, smsMessageId); - break; - - case CORRUPT_MESSAGE: - case NO_SESSION: - warn(String.valueOf(timestamp), "Discovered old enqueued bad encrypted message. Scheduling reset."); - ApplicationDependencies.getJobManager().add(new AutomaticSessionResetJob(sender.getId(), e.senderDevice, timestamp)); - break; - - default: - throw new AssertionError("Not handled " + messageState + ". (" + timestamp + ")"); - } - } - - private long getThreadIdForException(ExceptionMetadata metadata) { - if (metadata.groupId != null) { - Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(metadata.groupId); - return SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient); - } else { - return SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.external(context, metadata.sender)); - } - } - - private void handleCallOfferMessage(@NonNull SignalServiceContent content, - @NonNull OfferMessage message, - @NonNull Optional smsMessageId, - @NonNull Recipient senderRecipient) - { - log(String.valueOf(content.getTimestamp()), "handleCallOfferMessage..."); - - if (smsMessageId.isPresent()) { - MessageTable database = SignalDatabase.messages(); - database.markAsMissedCall(smsMessageId.get(), message.getType() == OfferMessage.Type.VIDEO_CALL); - } else { - RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId())); - byte[] remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipient.getId()).map(record -> record.getIdentityKey().serialize()).get(); - - ApplicationDependencies.getSignalCallManager() - .receivedOffer(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice()), - new WebRtcData.OfferMetadata(message.getOpaque(), message.getSdp(), message.getType()), - new WebRtcData.ReceivedOfferMetadata(remoteIdentityKey, - content.getServerReceivedTimestamp(), - content.getServerDeliveredTimestamp(), - content.getCallMessage().get().isMultiRing())); - } - } - - private void handleCallAnswerMessage(@NonNull SignalServiceContent content, - @NonNull AnswerMessage message, - @NonNull Recipient senderRecipient) - { - log(content.getTimestamp(), "handleCallAnswerMessage..."); - RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId())); - byte[] remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipient.getId()).map(record -> record.getIdentityKey().serialize()).get(); - - ApplicationDependencies.getSignalCallManager() - .receivedAnswer(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice()), - new WebRtcData.AnswerMetadata(message.getOpaque(), message.getSdp()), - new WebRtcData.ReceivedAnswerMetadata(remoteIdentityKey, content.getCallMessage().get().isMultiRing())); - } - - private void handleCallIceUpdateMessage(@NonNull SignalServiceContent content, - @NonNull List messages, - @NonNull Recipient senderRecipient) - { - log(content.getTimestamp(), "handleCallIceUpdateMessage... " + messages.size()); - - List iceCandidates = new ArrayList<>(messages.size()); - long callId = -1; - - for (IceUpdateMessage iceMessage : messages) { - iceCandidates.add(iceMessage.getOpaque()); - callId = iceMessage.getId(); - } - - RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(callId)); - - ApplicationDependencies.getSignalCallManager() - .receivedIceCandidates(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice()), - iceCandidates); - } - - private void handleCallHangupMessage(@NonNull SignalServiceContent content, - @NonNull HangupMessage message, - @NonNull Optional smsMessageId, - @NonNull Recipient senderRecipient) - { - log(content.getTimestamp(), "handleCallHangupMessage"); - if (smsMessageId.isPresent()) { - SignalDatabase.messages().markAsMissedCall(smsMessageId.get(), false); - } else { - RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId())); - - ApplicationDependencies.getSignalCallManager() - .receivedCallHangup(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice()), - new WebRtcData.HangupMetadata(message.getType(), message.isLegacy(), message.getDeviceId())); - } - } - - private void handleCallBusyMessage(@NonNull SignalServiceContent content, - @NonNull BusyMessage message, - @NonNull Recipient senderRecipient) - { - log(String.valueOf(content.getTimestamp()), "handleCallBusyMessage"); - - RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId())); - - ApplicationDependencies.getSignalCallManager() - .receivedCallBusy(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice())); - } - - private void handleCallOpaqueMessage(@NonNull SignalServiceContent content, - @NonNull OpaqueMessage message, - @NonNull Recipient senderRecipient) - { - log(String.valueOf(content.getTimestamp()), "handleCallOpaqueMessage"); - - long messageAgeSeconds = 0; - if (content.getServerReceivedTimestamp() > 0 && content.getServerDeliveredTimestamp() >= content.getServerReceivedTimestamp()) { - messageAgeSeconds = (content.getServerDeliveredTimestamp() - content.getServerReceivedTimestamp()) / 1000; - } - - ApplicationDependencies.getSignalCallManager() - .receivedOpaqueMessage(new WebRtcData.OpaqueMessageMetadata(senderRecipient.requireAci().getRawUuid(), - message.getOpaque(), - content.getSenderDevice(), - messageAgeSeconds)); - } - - private void handleGroupCallUpdateMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message, - @NonNull Optional groupId, - @NonNull Recipient senderRecipient) - { - log(content.getTimestamp(), "Group call update message."); - - if (!groupId.isPresent() || !groupId.get().isV2()) { - Log.w(TAG, "Invalid group for group call update message"); - return; - } - - RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromPossiblyMigratedGroupId(groupId.get()); - - SignalDatabase.calls().insertOrUpdateGroupCallFromExternalEvent(groupRecipientId, - senderRecipient.getId(), - content.getServerReceivedTimestamp(), - message.getGroupCallUpdate().get().getEraId()); - - GroupCallPeekJob.enqueue(groupRecipientId); - } - - private @Nullable MessageId handleEndSessionMessage(@NonNull SignalServiceContent content, - @NonNull Optional smsMessageId, - @NonNull Recipient senderRecipient) - { - log(content.getTimestamp(), "End session message."); - - MessageTable smsDatabase = SignalDatabase.messages(); - IncomingTextMessage incomingTextMessage = new IncomingTextMessage(senderRecipient.getId(), - content.getSenderDevice(), - content.getTimestamp(), - content.getServerReceivedTimestamp(), - System.currentTimeMillis(), - "", - Optional.empty(), - 0, - content.isNeedsReceipt(), - content.getServerUuid()); - - Optional insertResult; - - if (!smsMessageId.isPresent()) { - IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage); - - insertResult = smsDatabase.insertMessageInbox(incomingEndSessionMessage); - } else { - smsDatabase.markAsEndSession(smsMessageId.get()); - insertResult = Optional.of(new InsertResult(smsMessageId.get(), smsDatabase.getThreadIdForMessage(smsMessageId.get()), null)); - } - - if (insertResult.isPresent()) { - ApplicationDependencies.getProtocolStore().aci().deleteAllSessions(content.getSender().getIdentifier()); - - SecurityEvent.broadcastSecurityUpdateEvent(context); - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - - return new MessageId(insertResult.get().getMessageId()); - } else { - return null; - } - } - - private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) - throws MmsException - { - log(envelopeTimestamp, "Synchronize end session message."); - - MessageTable database = SignalDatabase.messages(); - Recipient recipient = getSyncMessageDestination(message); - OutgoingMessage outgoingEndSessionMessage = OutgoingMessage.endSessionMessage(recipient, message.getTimestamp()); - - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - - if (!recipient.isGroup()) { - ApplicationDependencies.getProtocolStore().aci().deleteAllSessions(recipient.requireServiceId().toString()); - - SecurityEvent.broadcastSecurityUpdateEvent(context); - - long messageId = database.insertMessageOutbox(outgoingEndSessionMessage, - threadId, - false, - null); - database.markAsSent(messageId, true); - SignalDatabase.threads().update(threadId, true); - } - - return threadId; - } - - private void handleUnknownGroupMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceGroupV2 group, - @NonNull Recipient senderRecipient) - throws BadGroupIdException - { - log(content.getTimestamp(), "Unknown group message."); - - warn(content.getTimestamp(), "Received a GV2 message for a group we have no knowledge of -- attempting to fix this state."); - ServiceId authServiceId = ServiceId.parseOrNull(content.getDestinationServiceId()); - if (authServiceId == null) { - warn(content.getTimestamp(), "Group message missing destination uuid, defaulting to ACI"); - authServiceId = SignalStore.account().requireAci(); - } - SignalDatabase.groups().fixMissingMasterKey(group.getMasterKey()); - } - - /** - * Inserts an expiration update if the message timer doesn't match the thread timer. - */ - private void handlePossibleExpirationUpdate(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message, - Optional groupId, - @NonNull Recipient senderRecipient, - @NonNull Recipient threadRecipient, - long receivedTime) - throws StorageFailedException - { - if (message.getExpiresInSeconds() != threadRecipient.getExpiresInSeconds()) { - warn(content.getTimestamp(), "Message expire time didn't match thread expire time. Handling timer update."); - handleExpirationUpdate(content, message, Optional.empty(), groupId, senderRecipient, threadRecipient, receivedTime, true); - } - } - - /** - * @param isActivatePaymentsRequest True if payments activation request message. - * @param isPaymentsActivated True if payments activated message. - * @throws StorageFailedException - */ - private @Nullable MessageId handlePaymentActivation(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message, - @NonNull Optional smsMessageId, - @NonNull Recipient senderRecipient, - long receivedTime, - boolean isActivatePaymentsRequest, - boolean isPaymentsActivated) - throws StorageFailedException - { - try { - MessageTable database = SignalDatabase.messages(); - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), - content.getTimestamp(), - content.getServerReceivedTimestamp(), - receivedTime, - StoryType.NONE, - null, - false, - -1, - TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()), - false, - false, - content.isNeedsReceipt(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - content.getServerUuid(), - null, - isActivatePaymentsRequest, - isPaymentsActivated); - - Optional insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); - if (smsMessageId.isPresent()) { - SignalDatabase.messages().deleteMessage(smsMessageId.get()); - } - - if (insertResult.isPresent()) { - return new MessageId(insertResult.get().getMessageId()); - } - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); - } - return null; - } - - /** - * @param sideEffect True if the event is side effect of a different message, false if the message itself was an expiration update. - * @throws StorageFailedException - */ - private @Nullable MessageId handleExpirationUpdate(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message, - @NonNull Optional smsMessageId, - @NonNull Optional groupId, - @NonNull Recipient senderRecipient, - @NonNull Recipient threadRecipient, - long receivedTime, - boolean sideEffect) - throws StorageFailedException - { - log(content.getTimestamp(), "Expiration update. Side effect: " + sideEffect); - - if (groupId.isPresent() && groupId.get().isV2()) { - warn(String.valueOf(content.getTimestamp()), "Expiration update received for GV2. Ignoring."); - return null; - } - - int expiresInSeconds = message.getExpiresInSeconds(); - Optional groupContext = message.getGroupContext(); - - if (threadRecipient.getExpiresInSeconds() == expiresInSeconds) { - log(String.valueOf(content.getTimestamp()), "No change in message expiry for group. Ignoring."); - return null; - } - - try { - MessageTable database = SignalDatabase.messages(); - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), - content.getTimestamp() - (sideEffect ? 1 : 0), - content.getServerReceivedTimestamp(), - receivedTime, - StoryType.NONE, - null, - false, - -1, - expiresInSeconds * 1000L, - true, - false, - content.isNeedsReceipt(), - Optional.empty(), - groupContext, - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - content.getServerUuid(), - null, - false, - false); - - Optional insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); - - SignalDatabase.recipients().setExpireMessages(threadRecipient.getId(), expiresInSeconds); - - if (smsMessageId.isPresent()) { - SignalDatabase.messages().deleteMessage(smsMessageId.get()); - } - - if (insertResult.isPresent()) { - return new MessageId(insertResult.get().getMessageId()); - } - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); - } - - return null; - } - - private @Nullable MessageId handleReaction(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient, boolean processingEarlyContent) throws StorageFailedException { - log(content.getTimestamp(), "Handle reaction for message " + message.getReaction().get().getTargetSentTimestamp()); - - SignalServiceDataMessage.Reaction reaction = message.getReaction().get(); - - if (!EmojiUtil.isEmoji(reaction.getEmoji())) { - Log.w(TAG, "Reaction text is not a valid emoji! Ignoring the message."); - return null; - } - - if (reaction.getTargetAuthor().isUnknown()) { - Log.w(TAG, "Invalid author UUID!"); - return null; - } - - Recipient targetAuthor = Recipient.externalPush(reaction.getTargetAuthor()); - MessageRecord targetMessage = SignalDatabase.messages().getMessageFor(reaction.getTargetSentTimestamp(), targetAuthor.getId()); - - if (targetMessage == null) { - warn(String.valueOf(content.getTimestamp()), "[handleReaction] Could not find matching message! Putting it in the early message cache. timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); - if (!processingEarlyContent) { - ApplicationDependencies.getEarlyMessageCache().store(targetAuthor.getId(), reaction.getTargetSentTimestamp(), content); - PushProcessEarlyMessagesJob.enqueue(); - } - return null; - } - - if (targetMessage.isRemoteDelete()) { - warn(String.valueOf(content.getTimestamp()), "[handleReaction] Found a matching message, but it's flagged as remotely deleted. timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); - return null; - } - - ThreadRecord targetThread = SignalDatabase.threads().getThreadRecord(targetMessage.getThreadId()); - - if (targetThread == null) { - warn(String.valueOf(content.getTimestamp()), "[handleReaction] Could not find a thread for the message! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); - return null; - } - - Recipient threadRecipient = targetThread.getRecipient().resolve(); - - if (threadRecipient.isGroup() && !threadRecipient.getParticipantIds().contains(senderRecipient.getId())) { - warn(String.valueOf(content.getTimestamp()), "[handleReaction] Reaction author is not in the group! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); - return null; - } - - if (!threadRecipient.isGroup() && !senderRecipient.equals(threadRecipient) && !senderRecipient.isSelf()) { - warn(String.valueOf(content.getTimestamp()), "[handleReaction] Reaction author is not a part of the 1:1 thread! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); - return null; - } - - MessageId targetMessageId = new MessageId(targetMessage.getId()); - - if (reaction.isRemove()) { - SignalDatabase.reactions().deleteReaction(targetMessageId, senderRecipient.getId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context); - } else { - ReactionRecord reactionRecord = new ReactionRecord(reaction.getEmoji(), senderRecipient.getId(), message.getTimestamp(), System.currentTimeMillis()); - SignalDatabase.reactions().addReaction(targetMessageId, reactionRecord); - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromMessageRecord(targetMessage), false); - } - - return new MessageId(targetMessage.getId()); - } - - private @Nullable MessageId handleRemoteDelete(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient, boolean processingEarlyContent) { - log(content.getTimestamp(), "Remote delete for message " + message.getRemoteDelete().get().getTargetSentTimestamp()); - - SignalServiceDataMessage.RemoteDelete delete = message.getRemoteDelete().get(); - - MessageRecord targetMessage = SignalDatabase.messages().getMessageFor(delete.getTargetSentTimestamp(), senderRecipient.getId()); - - if (targetMessage != null && MessageConstraintsUtil.isValidRemoteDeleteReceive(targetMessage, senderRecipient.getId(), content.getServerReceivedTimestamp())) { - MessageTable db = targetMessage.isMms() ? SignalDatabase.messages() : SignalDatabase.messages(); - db.markAsRemoteDelete(targetMessage.getId()); - if (MessageRecordUtil.isStory(targetMessage)) { - db.deleteRemotelyDeletedStory(targetMessage.getId()); - } - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromMessageRecord(targetMessage), false); - return new MessageId(targetMessage.getId()); - } else if (targetMessage == null) { - warn(String.valueOf(content.getTimestamp()), "[handleRemoteDelete] Could not find matching message! timestamp: " + delete.getTargetSentTimestamp() + " author: " + senderRecipient.getId()); - if (!processingEarlyContent) { - ApplicationDependencies.getEarlyMessageCache().store(senderRecipient.getId(), delete.getTargetSentTimestamp(), content); - PushProcessEarlyMessagesJob.enqueue(); - } - return null; - } else { - warn(String.valueOf(content.getTimestamp()), String.format(Locale.ENGLISH, "[handleRemoteDelete] Invalid remote delete! deleteTime: %d, targetTime: %d, deleteAuthor: %s, targetAuthor: %s", - content.getServerReceivedTimestamp(), targetMessage.getServerTimestamp(), senderRecipient.getId(), targetMessage.getFromRecipient().getId())); - return null; - } - } - - private void handleSynchronizeVerifiedMessage(@NonNull VerifiedMessage verifiedMessage) { - log(verifiedMessage.getTimestamp(), "Synchronize verified message."); - IdentityUtil.processVerifiedMessage(context, verifiedMessage); - } - - private void handleSynchronizeStickerPackOperation(@NonNull List stickerPackOperations, long envelopeTimestamp) { - log(envelopeTimestamp, "Synchronize sticker pack operation."); - - JobManager jobManager = ApplicationDependencies.getJobManager(); - - for (StickerPackOperationMessage operation : stickerPackOperations) { - if (operation.getPackId().isPresent() && operation.getPackKey().isPresent() && operation.getType().isPresent()) { - String packId = Hex.toStringCondensed(operation.getPackId().get()); - String packKey = Hex.toStringCondensed(operation.getPackKey().get()); - - switch (operation.getType().get()) { - case INSTALL: - jobManager.add(StickerPackDownloadJob.forInstall(packId, packKey, false)); - break; - case REMOVE: - SignalDatabase.stickers().uninstallPack(packId); - break; - } - } else { - warn("Received incomplete sticker pack operation sync."); - } - } - } - - private void handleSynchronizeConfigurationMessage(@NonNull ConfigurationMessage configurationMessage, long envelopeTimestamp) { - log(envelopeTimestamp, "Synchronize configuration message."); - - if (configurationMessage.getReadReceipts().isPresent()) { - TextSecurePreferences.setReadReceiptsEnabled(context, configurationMessage.getReadReceipts().get()); - } - - if (configurationMessage.getUnidentifiedDeliveryIndicators().isPresent()) { - TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, configurationMessage.getReadReceipts().get()); - } - - if (configurationMessage.getTypingIndicators().isPresent()) { - TextSecurePreferences.setTypingIndicatorsEnabled(context, configurationMessage.getTypingIndicators().get()); - } - - if (configurationMessage.getLinkPreviews().isPresent()) { - SignalStore.settings().setLinkPreviewsEnabled(configurationMessage.getReadReceipts().get()); - } - } - - private void handleSynchronizeBlockedListMessage(@NonNull BlockedListMessage blockMessage) { - SignalDatabase.recipients().applyBlockedUpdate(blockMessage.getAddresses(), blockMessage.getGroupIds()); - } - - private void handleSynchronizeFetchMessage(@NonNull SignalServiceSyncMessage.FetchType fetchType, long envelopeTimestamp) { - log(envelopeTimestamp, "Received fetch request with type: " + fetchType); - - switch (fetchType) { - case LOCAL_PROFILE: - ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob()); - break; - case STORAGE_MANIFEST: - StorageSyncHelper.scheduleSyncForDataChange(); - break; - case SUBSCRIPTION_STATUS: - warn(TAG, "Dropping subscription status fetch message."); - break; - default: - warn(TAG, "Received a fetch message for an unknown type."); - } - } - - private void handleSynchronizeMessageRequestResponse(@NonNull MessageRequestResponseMessage response, long envelopeTimestamp) - throws BadGroupIdException - { - log(envelopeTimestamp, "Synchronize message request response."); - - RecipientTable recipientTable = SignalDatabase.recipients(); - ThreadTable threadTable = SignalDatabase.threads(); - - Recipient recipient; - - if (response.getPerson().isPresent()) { - recipient = Recipient.externalPush(response.getPerson().get()); - } else if (response.getGroupId().isPresent()) { - GroupId groupId = GroupId.push(response.getGroupId().get()); - recipient = Recipient.externalPossiblyMigratedGroup(groupId); - } else { - warn("Message request response was missing a thread recipient! Skipping."); - return; - } - - long threadId = threadTable.getOrCreateThreadIdFor(recipient); - - switch (response.getType()) { - case ACCEPT: - recipientTable.setProfileSharing(recipient.getId(), true); - recipientTable.setBlocked(recipient.getId(), false); - break; - case DELETE: - recipientTable.setProfileSharing(recipient.getId(), false); - if (threadId > 0) threadTable.deleteConversation(threadId); - break; - case BLOCK: - recipientTable.setBlocked(recipient.getId(), true); - recipientTable.setProfileSharing(recipient.getId(), false); - break; - case BLOCK_AND_DELETE: - recipientTable.setBlocked(recipient.getId(), true); - recipientTable.setProfileSharing(recipient.getId(), false); - if (threadId > 0) threadTable.deleteConversation(threadId); - break; - default: - warn("Got an unknown response type! Skipping"); - break; - } - } - - private void handleSynchronizeOutgoingPayment(@NonNull SignalServiceContent content, @NonNull OutgoingPaymentMessage outgoingPaymentMessage) { - RecipientId recipientId = outgoingPaymentMessage.getRecipient() - .map(RecipientId::from) - .orElse(null); - long timestamp = outgoingPaymentMessage.getBlockTimestamp(); - if (timestamp == 0) { - timestamp = System.currentTimeMillis(); - } - - Optional address = outgoingPaymentMessage.getAddress().map(MobileCoinPublicAddress::fromBytes); - if (!address.isPresent() && recipientId == null) { - log(content.getTimestamp(), "Inserting defrag"); - address = Optional.of(ApplicationDependencies.getPayments().getWallet().getMobileCoinPublicAddress()); - recipientId = Recipient.self().getId(); - } - - UUID uuid = UUID.randomUUID(); - try { - SignalDatabase.payments() - .createSuccessfulPayment(uuid, - recipientId, - address.get(), - timestamp, - outgoingPaymentMessage.getBlockIndex(), - outgoingPaymentMessage.getNote().orElse(""), - outgoingPaymentMessage.getAmount(), - outgoingPaymentMessage.getFee(), - outgoingPaymentMessage.getReceipt().toByteArray(), - PaymentMetaDataUtil.fromKeysAndImages(outgoingPaymentMessage.getPublicKeys(), outgoingPaymentMessage.getKeyImages())); - } catch (SerializationException e) { - warn(content.getTimestamp(), "Ignoring synchronized outgoing payment with bad data.", e); - } - - log("Inserted synchronized payment " + uuid); - } - - private void handleSynchronizeKeys(@NonNull KeysMessage keysMessage, long envelopeTimestamp) { - if (SignalStore.account().isLinkedDevice()) { - log(envelopeTimestamp, "Synchronize keys."); - } else { - log(envelopeTimestamp, "Primary device ignores synchronize keys."); - return; - } - - SignalStore.storageService().setStorageKeyFromPrimary(keysMessage.getStorageService().get()); - } - - private void handleSynchronizeContacts(@NonNull ContactsMessage contactsMessage, long envelopeTimestamp) throws IOException { - if (SignalStore.account().isLinkedDevice()) { - log(envelopeTimestamp, "Synchronize contacts."); - } else { - log(envelopeTimestamp, "Primary device ignores synchronize contacts."); - return; - } - - if (!(contactsMessage.getContactsStream() instanceof SignalServiceAttachmentPointer)) { - warn(envelopeTimestamp, "No contact stream available."); - return; - } - - SignalServiceAttachmentPointer contactsAttachment = (SignalServiceAttachmentPointer) contactsMessage.getContactsStream(); - - ApplicationDependencies.getJobManager().add(new MultiDeviceContactSyncJob(contactsAttachment)); - } - - private void handleSynchronizeCallEvent(@NonNull SyncMessage.CallEvent callEvent, long envelopeTimestamp) { - if (!callEvent.hasId()) { - log(envelopeTimestamp, "Synchronize call event missing call id, ignoring."); - return; - } - - long callId = callEvent.getId(); - long timestamp = callEvent.getTimestamp(); - CallTable.Type type = CallTable.Type.from(callEvent.getType()); - CallTable.Direction direction = CallTable.Direction.from(callEvent.getDirection()); - CallTable.Event event = CallTable.Event.from(callEvent.getEvent()); - - if (timestamp == 0 || type == null || direction == null || event == null || !callEvent.hasConversationId()) { - warn(envelopeTimestamp, "Call event sync message is not valid, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId()); - return; - } - - ServiceId serviceId = ServiceId.parseOrThrow(callEvent.getConversationId()); - RecipientId recipientId = RecipientId.from(serviceId); - - log(envelopeTimestamp, "Synchronize call event call: " + callId); - - CallTable.Call call = SignalDatabase.calls().getCallById(callId, recipientId); - if (call != null) { - boolean typeMismatch = call.getType() != type; - boolean directionMismatch = call.getDirection() != direction; - boolean eventDowngrade = call.getEvent() == CallTable.Event.ACCEPTED && event != CallTable.Event.ACCEPTED; - boolean peerMismatch = !call.getPeer().equals(recipientId); - - if (typeMismatch || directionMismatch || eventDowngrade || peerMismatch) { - warn(envelopeTimestamp, "Call event sync message is not valid for existing call record, ignoring. type: " + type + " direction: " + direction + " event: " + event + " peerMismatch: " + peerMismatch); - } else { - SignalDatabase.calls().updateOneToOneCall(callId, event); - } - } else { - SignalDatabase.calls().insertOneToOneCall(callId, timestamp, recipientId, type, direction, event); - } - } - - private void handleSynchronizeGroupOrAdHocCallEvent(@NonNull SyncMessage.CallEvent callEvent, long envelopeTimestamp) - throws BadGroupIdException - { - if (!callEvent.hasId()) { - log(envelopeTimestamp, "Synchronize group/ad-hoc call event missing call id, ignoring."); - return; - } - - if (!FeatureFlags.adHocCalling() && callEvent.getType() == SyncMessage.CallEvent.Type.AD_HOC_CALL) { - log(envelopeTimestamp, "Ad-Hoc calling is not currently supported by this client, ignoring."); - return; - } - - long callId = callEvent.getId(); - long timestamp = callEvent.getTimestamp(); - CallTable.Type type = CallTable.Type.from(callEvent.getType()); - CallTable.Direction direction = CallTable.Direction.from(callEvent.getDirection()); - CallTable.Event event = CallTable.Event.from(callEvent.getEvent()); - - if (timestamp == 0 || type == null || direction == null || event == null || !callEvent.hasConversationId()) { - warn(envelopeTimestamp, "Group/Ad-hoc call event sync message is not valid, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId()); - return; - } - - RecipientId recipientId; - switch (type) { - case AD_HOC_CALL: - CallLinkRoomId callLinkRoomId = CallLinkRoomId.fromBytes(callEvent.getConversationId().toByteArray()); - - recipientId = SignalDatabase.recipients().getByCallLinkRoomId(callLinkRoomId).orElse(null); - break; - case GROUP_CALL: - GroupId groupId = GroupId.push(callEvent.getConversationId().toByteArray()); - - recipientId = Recipient.externalGroupExact(groupId).getId(); - break; - default: - warn(envelopeTimestamp, "Group/Ad-hoc call event has a bad type " + type + ". Ignoring."); - return; - } - - if (recipientId == null) { - Log.w(TAG, "Could not find a matching group or ad-hoc call. Dropping sync event."); - return; - } - - CallTable.Call call = SignalDatabase.calls().getCallById(callId, recipientId); - if (call != null) { - if (call.getType() != type) { - warn(envelopeTimestamp, "Group/Ad-hoc call event type mismatch, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId()); - return; - } - - switch (event) { - case DELETE: - SignalDatabase.calls().deleteGroupCall(call); - break; - case ACCEPTED: - if (call.getTimestamp() < callEvent.getTimestamp()) { - SignalDatabase.calls().setTimestamp(call.getCallId(), recipientId, callEvent.getTimestamp()); - } - - if (callEvent.getDirection() == SyncMessage.CallEvent.Direction.INCOMING) { - SignalDatabase.calls().acceptIncomingGroupCall(call); - } else { - warn(envelopeTimestamp, "Invalid direction OUTGOING for event ACCEPTED"); - } - - break; - case NOT_ACCEPTED: - default: - warn("Unsupported event type " + event + ". Ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId()); - } - } else { - switch (event) { - case DELETE: - SignalDatabase.calls().insertDeletedGroupCallFromSyncEvent(callEvent.getId(), recipientId, direction, timestamp); - break; - case ACCEPTED: - SignalDatabase.calls().insertAcceptedGroupCall(callEvent.getId(), recipientId, direction, timestamp); - break; - case NOT_ACCEPTED: - default: - warn("Unsupported event type " + event + ". Ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId()); - } - } - } - - private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content, - @NonNull SentTranscriptMessage message, - @NonNull Recipient senderRecipient, - boolean processingEarlyContent) - throws StorageFailedException, BadGroupIdException, IOException, GroupChangeBusyException - { - log(String.valueOf(content.getTimestamp()), "Processing sent transcript for message with ID " + message.getTimestamp()); - - try { - GroupTable groupDatabase = SignalDatabase.groups(); - - if (message.getStoryMessage().isPresent() || !message.getStoryMessageRecipients().isEmpty()) { - handleSynchronizeSentStoryMessage(message, content.getTimestamp()); - return; - } - - SignalServiceDataMessage dataMessage = message.getDataMessage().get(); - if (dataMessage.isGroupV2Message()) { - GroupId.V2 groupId = GroupId.v2(dataMessage.getGroupContext().get().getMasterKey()); - if (handleGv2PreProcessing(groupId, content, dataMessage.getGroupContext().get(), senderRecipient)) { - return; - } - } - - long threadId = -1; - - if (message.isRecipientUpdate()) { - handleGroupRecipientUpdate(message, content.getTimestamp()); - } else if (dataMessage.isEndSession()) { - threadId = handleSynchronizeSentEndSessionMessage(message, content.getTimestamp()); - } else if (dataMessage.isGroupV2Update()) { - handleSynchronizeSentGv2Update(content, message); - threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message)); - } else if (dataMessage.getGroupCallUpdate().isPresent()) { - handleGroupCallUpdateMessage(content, dataMessage, GroupUtil.idFromGroupContext(dataMessage.getGroupContext()), senderRecipient); - } else if (dataMessage.isEmptyGroupV2Message()) { - warn(content.getTimestamp(), "Empty GV2 message! Doing nothing."); - } else if (dataMessage.isExpirationUpdate()) { - threadId = handleSynchronizeSentExpirationUpdate(message); - } else if (dataMessage.getStoryContext().isPresent()) { - threadId = handleSynchronizeSentStoryReply(message, content.getTimestamp()); - } else if (dataMessage.getReaction().isPresent()) { - handleReaction(content, dataMessage, senderRecipient, processingEarlyContent); - threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message)); - } else if (dataMessage.getRemoteDelete().isPresent()) { - handleRemoteDelete(content, dataMessage, senderRecipient, processingEarlyContent); - } else if (dataMessage.getAttachments().isPresent() || dataMessage.getQuote().isPresent() || dataMessage.getPreviews().isPresent() || dataMessage.getSticker().isPresent() || dataMessage.isViewOnce() || dataMessage.getMentions().isPresent() || dataMessage.getBodyRanges().isPresent()) { - threadId = handleSynchronizeSentMediaMessage(message, content.getTimestamp()); - } else { - threadId = handleSynchronizeSentTextMessage(message, content.getTimestamp()); - } - - if (dataMessage.getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupId.v2(dataMessage.getGroupContext().get().getMasterKey()))) { - handleUnknownGroupMessage(content, dataMessage.getGroupContext().get(), senderRecipient); - } - - if (dataMessage.getProfileKey().isPresent()) { - Recipient recipient = getSyncMessageDestination(message); - - if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) { - SignalDatabase.recipients().setProfileSharing(recipient.getId(), true); - } - } - - if (threadId != -1) { - SignalDatabase.threads().setRead(threadId, true); - ApplicationDependencies.getMessageNotifier().updateNotification(context); - } - - if (SignalStore.rateLimit().needsRecaptcha()) { - log(content.getTimestamp(), "Got a sent transcript while in reCAPTCHA mode. Assuming we're good to message again."); - RateLimitUtil.retryAllRateLimitedMessages(context); - } - - ApplicationDependencies.getMessageNotifier().setLastDesktopActivityTimestamp(message.getTimestamp()); - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); - } - } - - private void handleSynchronizeSentGv2Update(@NonNull SignalServiceContent content, - @NonNull SentTranscriptMessage message) - throws IOException, GroupChangeBusyException - { - log(content.getTimestamp(), "Synchronize sent GV2 update for message with timestamp " + message.getTimestamp()); - - SignalServiceDataMessage dataMessage = message.getDataMessage().get(); - SignalServiceGroupV2 signalServiceGroupV2 = dataMessage.getGroupContext().get(); - GroupId.V2 groupIdV2 = GroupId.v2(signalServiceGroupV2.getMasterKey()); - - if (!updateGv2GroupFromServerOrP2PChange(content, signalServiceGroupV2)) { - log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupIdV2); - } - } - - private void handleSynchronizeRequestMessage(@NonNull RequestMessage message, long envelopeTimestamp) - { - if (SignalStore.account().isPrimaryDevice()) { - log(envelopeTimestamp, "Synchronize request message."); - } else { - log(envelopeTimestamp, "Linked device ignoring synchronize request message."); - return; - } - - if (message.isContactsRequest()) { - ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true)); - } - - if (message.isBlockedListRequest()) { - ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob()); - } - - if (message.isConfigurationRequest()) { - ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(context), - TextSecurePreferences.isTypingIndicatorsEnabled(context), - TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context), - SignalStore.settings().isLinkPreviewsEnabled())); - ApplicationDependencies.getJobManager().add(new MultiDeviceStickerPackSyncJob()); - } - - if (message.isKeysRequest()) { - ApplicationDependencies.getJobManager().add(new MultiDeviceKeysUpdateJob()); - } - } - - private void handleSynchronizeReadMessage(@NonNull SignalServiceContent content, - @NonNull List readMessages, - long envelopeTimestamp, - boolean processingEarlyContent) - { - log(envelopeTimestamp, "Synchronize read message. Count: " + readMessages.size() + ", Timestamps: " + Stream.of(readMessages).map(ReadMessage::getTimestamp).toList()); - - Map threadToLatestRead = new HashMap<>(); - - Collection unhandled = SignalDatabase.messages().setTimestampReadFromSyncMessage(readMessages, envelopeTimestamp, threadToLatestRead); - - List markedMessages = SignalDatabase.threads().setReadSince(threadToLatestRead, false); - - if (Util.hasItems(markedMessages)) { - Log.i(TAG, "Updating past messages: " + markedMessages.size()); - MarkReadReceiver.process(markedMessages); - } - - for (SyncMessageId id : unhandled) { - warn(String.valueOf(content.getTimestamp()), "[handleSynchronizeReadMessage] Could not find matching message! timestamp: " + id.getTimetamp() + " author: " + id.getRecipientId()); - if (!processingEarlyContent) { - ApplicationDependencies.getEarlyMessageCache().store(id.getRecipientId(), id.getTimetamp(), content); - } - } - - if (unhandled.size() > 0 && !processingEarlyContent) { - PushProcessEarlyMessagesJob.enqueue(); - } - - MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier(); - messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp); - messageNotifier.cancelDelayedNotifications(); - messageNotifier.updateNotification(context); - } - - private void handleSynchronizeViewedMessage(@NonNull List viewedMessages, long envelopeTimestamp) { - log(envelopeTimestamp, "Synchronize view message. Count: " + viewedMessages.size() + ", Timestamps: " + Stream.of(viewedMessages).map(ViewedMessage::getTimestamp).toList()); - - List records = Stream.of(viewedMessages) - .map(message -> { - RecipientId author = Recipient.externalPush(message.getSender()).getId(); - return SignalDatabase.messages().getMessageFor(message.getTimestamp(), author); - }) - .filter(message -> message != null && message.isMms()) - .toList(); - - List toMarkViewed = Stream.of(records) - .map(MessageRecord::getId) - .toList(); - - List toEnqueueDownload = Stream.of(records) - .filter(MessageRecord::isMms) - .map(it -> (MediaMmsMessageRecord) it) - .filter(it -> it.getStoryType().isStory() && !it.getStoryType().isTextStory()) - .toList(); - - for (final MediaMmsMessageRecord mediaMmsMessageRecord : toEnqueueDownload) { - Stories.enqueueAttachmentsFromStoryForDownloadSync(mediaMmsMessageRecord, false); - } - - SignalDatabase.messages().setIncomingMessagesViewed(toMarkViewed); - SignalDatabase.messages().setOutgoingGiftsRevealed(toMarkViewed); - - MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier(); - messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp); - messageNotifier.cancelDelayedNotifications(); - messageNotifier.updateNotification(context); - } - - private void handleSynchronizeViewOnceOpenMessage(@NonNull SignalServiceContent content, @NonNull ViewOnceOpenMessage openMessage, long envelopeTimestamp, boolean processingEarlyContent) { - log(envelopeTimestamp, "Handling a view-once open for message: " + openMessage.getTimestamp()); - - RecipientId author = Recipient.externalPush(openMessage.getSender()).getId(); - long timestamp = openMessage.getTimestamp(); - MessageRecord record = SignalDatabase.messages().getMessageFor(timestamp, author); - - if (record != null && record.isMms()) { - SignalDatabase.attachments().deleteAttachmentFilesForViewOnceMessage(record.getId()); - } else { - warn(String.valueOf(envelopeTimestamp), "Got a view-once open message for a message we don't have!"); - - if (!processingEarlyContent) { - ApplicationDependencies.getEarlyMessageCache().store(author, timestamp, content); - PushProcessEarlyMessagesJob.enqueue(); - } - } - - MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier(); - messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp); - messageNotifier.cancelDelayedNotifications(); - messageNotifier.updateNotification(context); - } - - private void handleStoryMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceStoryMessage message, @NonNull Recipient senderRecipient, @NonNull Recipient threadRecipient) throws StorageFailedException { - log(content.getTimestamp(), "Story message."); - - if (threadRecipient.isInactiveGroup()) { - warn(content.getTimestamp(), "Dropping a group story from a group we're no longer in."); - return; - } - - if (threadRecipient.isGroup() && !SignalDatabase.groups().isCurrentMember(threadRecipient.requireGroupId().requirePush(), senderRecipient.getId())) { - warn(content.getTimestamp(), "Dropping a group story from a user who's no longer a member."); - return; - } - - if (!threadRecipient.isGroup() && !(senderRecipient.isProfileSharing() || senderRecipient.isSystemContact())) { - warn(content.getTimestamp(), "Dropping story from an untrusted source."); - return; - } - - Optional insertResult; - - MessageTable database = SignalDatabase.messages(); - database.beginTransaction(); - - try { - final StoryType storyType; - if (message.getAllowsReplies().orElse(false)) { - storyType = StoryType.withReplies(message.getTextAttachment().isPresent()); - } else { - storyType = StoryType.withoutReplies(message.getTextAttachment().isPresent()); - } - - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), - content.getTimestamp(), - content.getServerReceivedTimestamp(), - System.currentTimeMillis(), - storyType, - null, - false, - -1, - 0, - false, - false, - content.isNeedsReceipt(), - message.getTextAttachment().map(this::serializeTextAttachment), - Optional.ofNullable(GroupUtil.getGroupContextIfPresent(content)), - message.getFileAttachment().map(Collections::singletonList), - Optional.empty(), - Optional.empty(), - getLinkPreviews(message.getTextAttachment().flatMap(t -> t.getPreview().map(Collections::singletonList)), - "", - true), - Optional.empty(), - Optional.empty(), - content.getServerUuid(), - null, - false, - false, - getBodyRangeList(message.getBodyRanges())); - - insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); - - if (insertResult.isPresent()) { - database.setTransactionSuccessful(); - } - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); - } finally { - database.endTransaction(); - } - - if (insertResult.isPresent()) { - Stories.enqueueNextStoriesForDownload(threadRecipient.getId(), false, FeatureFlags.storiesAutoDownloadMaximum()); - ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary(); - } - } - - private @NonNull String serializeTextAttachment(@NonNull SignalServiceTextAttachment textAttachment) { - StoryTextPost.Builder builder = StoryTextPost.newBuilder(); - - if (textAttachment.getText().isPresent()) { - builder.setBody(textAttachment.getText().get()); - } - - if (textAttachment.getStyle().isPresent()) { - switch (textAttachment.getStyle().get()) { - case DEFAULT: - builder.setStyle(StoryTextPost.Style.DEFAULT); - break; - case REGULAR: - builder.setStyle(StoryTextPost.Style.REGULAR); - break; - case BOLD: - builder.setStyle(StoryTextPost.Style.BOLD); - break; - case SERIF: - builder.setStyle(StoryTextPost.Style.SERIF); - break; - case SCRIPT: - builder.setStyle(StoryTextPost.Style.SCRIPT); - break; - case CONDENSED: - builder.setStyle(StoryTextPost.Style.CONDENSED); - break; - } - } - - if (textAttachment.getTextBackgroundColor().isPresent()) { - builder.setTextBackgroundColor(textAttachment.getTextBackgroundColor().get()); - } - - if (textAttachment.getTextForegroundColor().isPresent()) { - builder.setTextForegroundColor(textAttachment.getTextForegroundColor().get()); - } - - ChatColor.Builder chatColorBuilder = ChatColor.newBuilder(); - if (textAttachment.getBackgroundColor().isPresent()) { - chatColorBuilder.setSingleColor(ChatColor.SingleColor.newBuilder().setColor(textAttachment.getBackgroundColor().get())); - } else if (textAttachment.getBackgroundGradient().isPresent()) { - SignalServiceTextAttachment.Gradient gradient = textAttachment.getBackgroundGradient().get(); - ChatColor.LinearGradient.Builder linearGradientBuilder = ChatColor.LinearGradient.newBuilder(); - - linearGradientBuilder.setRotation(gradient.getAngle().orElse(0).floatValue()); - - if (gradient.getPositions().size() > 1 && gradient.getColors().size() == gradient.getPositions().size()) { - ArrayList positions = new ArrayList<>(gradient.getPositions()); - - positions.set(0, 0f); - positions.set(positions.size() - 1, 1f); - - linearGradientBuilder.addAllColors(new ArrayList<>(gradient.getColors())); - linearGradientBuilder.addAllPositions(positions); - } else if (!gradient.getColors().isEmpty()) { - Log.w(TAG, "Incoming text story has color / position mismatch. Defaulting to start and end colors."); - linearGradientBuilder.addColors(gradient.getColors().get(0)); - linearGradientBuilder.addColors(gradient.getColors().get(gradient.getColors().size() - 1)); - linearGradientBuilder.addAllPositions(Arrays.asList(0f, 1f)); - } else { - Log.w(TAG, "Incoming text story did not have a valid linear gradient."); - linearGradientBuilder.addAllColors(Arrays.asList(Color.BLACK, Color.BLACK)); - linearGradientBuilder.addAllPositions(Arrays.asList(0f, 1f)); - } - - chatColorBuilder.setLinearGradient(linearGradientBuilder); - } - - builder.setBackground(chatColorBuilder); - - return Base64.encodeBytes(builder.build().toByteArray()); - } - - private @Nullable MessageId handleStoryReaction(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient) throws StorageFailedException { - log(content.getTimestamp(), "Story reaction."); - - SignalServiceDataMessage.Reaction reaction = message.getReaction().get(); - - if (!EmojiUtil.isEmoji(reaction.getEmoji())) { - warn(content.getTimestamp(), "Story reaction text is not a valid emoji! Ignoring the message."); - return null; - } - - SignalServiceDataMessage.StoryContext storyContext = message.getStoryContext().get(); - - MessageTable database = SignalDatabase.messages(); - database.beginTransaction(); - - try { - RecipientId storyAuthorRecipient = RecipientId.from(storyContext.getAuthorServiceId()); - ParentStoryId parentStoryId; - QuoteModel quoteModel = null; - long expiresInMillis = 0; - try { - MessageId storyMessageId = database.getStoryId(storyAuthorRecipient, storyContext.getSentTimestamp()); - - if (message.getGroupContext().isPresent()) { - parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId()); - } else if (SignalDatabase.storySends().canReply(senderRecipient.getId(), storyContext.getSentTimestamp())) { - MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId()); - - String displayText = ""; - BodyRangeList bodyRanges = null; - if (story.getStoryType().isTextStory()) { - displayText = story.getBody(); - bodyRanges = story.getMessageRanges(); - } - - parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId()); - quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, bodyRanges); - expiresInMillis = TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()); - } else { - warn(content.getTimestamp(), "Story has reactions disabled. Dropping reaction."); - return null; - } - } catch (NoSuchMessageException e) { - warn(content.getTimestamp(), "Couldn't find story for reaction.", e); - return null; - } - - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), - content.getTimestamp(), - content.getServerReceivedTimestamp(), - System.currentTimeMillis(), - StoryType.NONE, - parentStoryId, - true, - -1, - expiresInMillis, - false, - false, - content.isNeedsReceipt(), - Optional.of(reaction.getEmoji()), - Optional.ofNullable(GroupUtil.getGroupContextIfPresent(content)), - Optional.empty(), - Optional.ofNullable(quoteModel), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - content.getServerUuid(), - null, - false, - false); - - Optional insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); - - if (insertResult.isPresent()) { - database.setTransactionSuccessful(); - - if (parentStoryId.isGroupReply()) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromThreadAndReply(insertResult.get().getThreadId(), (ParentStoryId.GroupReply) parentStoryId)); - } else { - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); - } - - if (parentStoryId.isDirectReply()) { - return MessageId.fromNullable(insertResult.get().getMessageId()); - } else { - return null; - } - } else { - warn(content.getTimestamp(), "Failed to insert story reaction"); - return null; - } - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); - } finally { - database.endTransaction(); - } - } - - private @Nullable MessageId handleStoryReply(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient, long receivedTime) throws StorageFailedException { - log(content.getTimestamp(), "Story reply."); - - SignalServiceDataMessage.StoryContext storyContext = message.getStoryContext().get(); - - MessageTable database = SignalDatabase.messages(); - database.beginTransaction(); - - try { - RecipientId storyAuthorRecipient = RecipientId.from(storyContext.getAuthorServiceId()); - RecipientId selfId = Recipient.self().getId(); - ParentStoryId parentStoryId; - QuoteModel quoteModel = null; - long expiresInMillis = 0L; - MessageId storyMessageId = null; - - try { - if (selfId.equals(storyAuthorRecipient)) { - storyMessageId = SignalDatabase.storySends().getStoryMessageFor(senderRecipient.getId(), storyContext.getSentTimestamp()); - } - if (storyMessageId == null) { - storyMessageId = database.getStoryId(storyAuthorRecipient, storyContext.getSentTimestamp()); - } - - MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId()); - Recipient threadRecipient = Objects.requireNonNull(SignalDatabase.threads().getRecipientForThreadId(story.getThreadId())); - boolean groupStory = threadRecipient.isActiveGroup(); - - if (!groupStory) { - threadRecipient = senderRecipient; - } - - handlePossibleExpirationUpdate(content, message, threadRecipient.getGroupId(), senderRecipient, threadRecipient, receivedTime); - - if (message.getGroupContext().isPresent() ) { - parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId()); - } else if (groupStory || SignalDatabase.storySends().canReply(senderRecipient.getId(), storyContext.getSentTimestamp())) { - parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId()); - - String displayText = ""; - BodyRangeList bodyRanges = null; - if (story.getStoryType().isTextStory()) { - displayText = story.getBody(); - bodyRanges = story.getMessageRanges(); - } - - quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, bodyRanges); - expiresInMillis = TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()); - } else { - warn(content.getTimestamp(), "Story has replies disabled. Dropping reply."); - return null; - } - } catch (NoSuchMessageException e) { - warn(content.getTimestamp(), "Couldn't find story for reply.", e); - return null; - } - - BodyRangeList bodyRanges = message.getBodyRanges().map(DatabaseProtosUtil::toBodyRangeList).orElse(null); - - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), - content.getTimestamp(), - content.getServerReceivedTimestamp(), - System.currentTimeMillis(), - StoryType.NONE, - parentStoryId, - false, - -1, - expiresInMillis, - false, - false, - content.isNeedsReceipt(), - message.getBody(), - Optional.ofNullable(GroupUtil.getGroupContextIfPresent(content)), - Optional.empty(), - Optional.ofNullable(quoteModel), - Optional.empty(), - Optional.empty(), - getMentions(message.getMentions()), - Optional.empty(), - content.getServerUuid(), - null, - false, - false, - bodyRanges); - - Optional insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); - - if (insertResult.isPresent()) { - database.setTransactionSuccessful(); - - if (parentStoryId.isGroupReply()) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromThreadAndReply(insertResult.get().getThreadId(), (ParentStoryId.GroupReply) parentStoryId)); - } else { - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); - } - - if (parentStoryId.isDirectReply()) { - return MessageId.fromNullable(insertResult.get().getMessageId()); - } else { - return null; - } - } else { - warn(content.getTimestamp(), "Failed to insert story reply."); - return null; - } - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); - } finally { - database.endTransaction(); - } - } - - private @Nullable MessageId handleGiftMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message, - @NonNull Recipient senderRecipient, - @NonNull Recipient threadRecipient, - long receivedTime) - throws StorageFailedException - { - log(message.getTimestamp(), "Gift message."); - - notifyTypingStoppedFromIncomingMessage(senderRecipient, threadRecipient, content.getSenderDevice()); - - Optional insertResult; - - MessageTable database = SignalDatabase.messages(); - - byte[] token = message.getGiftBadge().get().getReceiptCredentialPresentation().serialize(); - GiftBadge giftBadge = GiftBadge.newBuilder() - .setRedemptionToken(ByteString.copyFrom(token)) - .setRedemptionState(GiftBadge.RedemptionState.PENDING) - .build(); - - try { - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), - message.getTimestamp(), - content.getServerReceivedTimestamp(), - receivedTime, - StoryType.NONE, - null, - false, - -1, - TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()), - false, - false, - content.isNeedsReceipt(), - Optional.of(Base64.encodeBytes(giftBadge.toByteArray())), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - content.getServerUuid(), - giftBadge, - false, - false); - - insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); - } - - if (insertResult.isPresent()) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); - - return new MessageId(insertResult.get().getMessageId()); - } else { - return null; - } - } - - private @Nullable MessageId handleMediaMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message, - @NonNull Optional smsMessageId, - @NonNull Recipient senderRecipient, - @NonNull Recipient threadRecipient, - long receivedTime) - throws StorageFailedException - { - log(message.getTimestamp(), "Media message."); - - notifyTypingStoppedFromIncomingMessage(senderRecipient, threadRecipient, content.getSenderDevice()); - - Optional insertResult; - - MessageTable database = SignalDatabase.messages(); - database.beginTransaction(); - - try { - Optional quote = getValidatedQuote(message.getQuote()); - Optional> sharedContacts = getContacts(message.getSharedContacts()); - Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().orElse(""), false); - Optional> mentions = getMentions(message.getMentions()); - Optional sticker = getStickerAttachment(message.getSticker()); - BodyRangeList messageRanges = getBodyRangeList(message.getBodyRanges()); - - handlePossibleExpirationUpdate(content, message, Optional.empty(), senderRecipient, threadRecipient, receivedTime); - - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), - message.getTimestamp(), - content.getServerReceivedTimestamp(), - receivedTime, - StoryType.NONE, - null, - false, - -1, - TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()), - false, - message.isViewOnce(), - content.isNeedsReceipt(), - message.getBody(), - message.getGroupContext(), - message.getAttachments(), - quote, - sharedContacts, - linkPreviews, - mentions, - sticker, - content.getServerUuid(), - null, - false, - false, - messageRanges); - - insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); - - if (insertResult.isPresent()) { - if (smsMessageId.isPresent()) { - SignalDatabase.messages().deleteMessage(smsMessageId.get()); - } - - database.setTransactionSuccessful(); - } - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); - } finally { - database.endTransaction(); - } - - if (insertResult.isPresent()) { - List allAttachments = SignalDatabase.attachments().getAttachmentsForMessage(insertResult.get().getMessageId()); - List stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); - List attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); - - forceStickerDownloadIfNecessary(insertResult.get().getMessageId(), stickerAttachments); - - for (DatabaseAttachment attachment : attachments) { - ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); - } - - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); - - if (message.isViewOnce()) { - ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary(); - } - - return new MessageId(insertResult.get().getMessageId()); - } else { - return null; - } - } - - private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message) - throws MmsException - { - log(message.getTimestamp(), "Synchronize sent expiration update."); - - Optional groupId = getSyncMessageDestination(message).getGroupId(); - - if (groupId.isPresent() && groupId.get().isV2()) { - warn(String.valueOf(message.getTimestamp()), "Expiration update received for GV2. Ignoring."); - return -1; - } - - MessageTable database = SignalDatabase.messages(); - Recipient recipient = getSyncMessageDestination(message); - - OutgoingMessage expirationUpdateMessage = OutgoingMessage.expirationUpdateMessage(recipient, - message.getTimestamp(), - TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds())); - - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null); - - database.markAsSent(messageId, true); - - SignalDatabase.recipients().setExpireMessages(recipient.getId(), message.getDataMessage().get().getExpiresInSeconds()); - - return threadId; - } - - /** - * Handles both story replies and reactions. - */ - private long handleSynchronizeSentStoryReply(@NonNull SentTranscriptMessage message, long envelopeTimestamp) - throws MmsException, BadGroupIdException { - - log(envelopeTimestamp, "Synchronize sent story reply for " + message.getTimestamp()); - - try { - Optional reaction = message.getDataMessage().get().getReaction(); - ParentStoryId parentStoryId; - SignalServiceDataMessage.StoryContext storyContext = message.getDataMessage().get().getStoryContext().get(); - MessageTable database = SignalDatabase.messages(); - Recipient recipient = getSyncMessageDestination(message); - QuoteModel quoteModel = null; - long expiresInMillis = 0L; - RecipientId storyAuthorRecipient = RecipientId.from(storyContext.getAuthorServiceId()); - MessageId storyMessageId = database.getStoryId(storyAuthorRecipient, storyContext.getSentTimestamp()); - MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId()); - Recipient threadRecipient = SignalDatabase.threads().getRecipientForThreadId(story.getThreadId()); - boolean groupStory = threadRecipient != null && threadRecipient.isActiveGroup(); - BodyRangeList bodyRanges = null; - String body; - - if (reaction.isPresent() && EmojiUtil.isEmoji(reaction.get().getEmoji())) { - body = reaction.get().getEmoji(); - } else { - body = message.getDataMessage().get().getBody().orElse(null); - bodyRanges = getBodyRangeList(message.getDataMessage().get().getBodyRanges()); - } - - if (message.getDataMessage().get().getGroupContext().isPresent()) { - parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId()); - } else if (groupStory || story.getStoryType().isStoryWithReplies()) { - parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId()); - - String quoteBody = ""; - BodyRangeList quotedBodyRanges = null; - - if (story.getStoryType().isTextStory()) { - quoteBody = story.getBody(); - quotedBodyRanges = story.getMessageRanges(); - } - - quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, quoteBody, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, quotedBodyRanges); - expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()); - } else { - warn(envelopeTimestamp, "Story has replies disabled. Dropping reply."); - return -1L; - } - - OutgoingMessage mediaMessage = new OutgoingMessage(recipient, - body, - Collections.emptyList(), - message.getTimestamp(), - expiresInMillis, - false, - ThreadTable.DistributionTypes.DEFAULT, - StoryType.NONE, - parentStoryId, - message.getDataMessage().get().getReaction().isPresent(), - quoteModel, - Collections.emptyList(), - Collections.emptyList(), - getMentions(message.getDataMessage().get().getMentions()).orElse(Collections.emptyList()), - Collections.emptySet(), - Collections.emptySet(), - null, - true, - bodyRanges, - -1, - 0); - - if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) { - handleSynchronizeSentExpirationUpdate(message); - } - - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - long messageId; - - database.beginTransaction(); - try { - messageId = database.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null); - - if (recipient.isGroup()) { - updateGroupReceiptStatus(message, messageId, recipient.requireGroupId()); - } else { - database.markUnidentified(messageId, isUnidentified(message, recipient)); - } - - database.markAsSent(messageId, true); - - if (message.getDataMessage().get().getExpiresInSeconds() > 0) { - database.markExpireStarted(messageId, message.getExpirationStartTimestamp()); - ApplicationDependencies.getExpiringMessageManager() - .scheduleDeletion(messageId, - true, - message.getExpirationStartTimestamp(), - TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds())); - } - - if (recipient.isSelf()) { - SignalDatabase.messages().incrementDeliveryReceiptCount(message.getTimestamp(), recipient.getId(), System.currentTimeMillis()); - SignalDatabase.messages().incrementReadReceiptCount(message.getTimestamp(), recipient.getId(), System.currentTimeMillis()); - } - - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - - return threadId; - } catch (NoSuchMessageException e) { - warn(envelopeTimestamp, "Couldn't find story for reply.", e); - return -1L; - } - } - - private void handleSynchronizeSentStoryMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) throws MmsException { - log(envelopeTimestamp, "Synchronize sent story message for " + message.getTimestamp()); - - SentStorySyncManifest manifest = SentStorySyncManifest.fromRecipientsSet(message.getStoryMessageRecipients()); - - if (message.isRecipientUpdate()) { - log(envelopeTimestamp, "Processing recipient update for story message and exiting..."); - SignalDatabase.storySends().applySentStoryManifest(manifest, message.getTimestamp()); - return; - } - - SignalServiceStoryMessage storyMessage = message.getStoryMessage().get(); - Set distributionIds = manifest.getDistributionIdSet(); - Optional groupId = storyMessage.getGroupContext().map(it -> GroupId.v2(it.getMasterKey())); - String textStoryBody = storyMessage.getTextAttachment().map(this::serializeTextAttachment).orElse(null); - BodyRangeList bodyRanges = getBodyRangeList(storyMessage.getBodyRanges()); - StoryType storyType = getStoryType(storyMessage); - List linkPreviews = getLinkPreviews(storyMessage.getTextAttachment().flatMap(t -> t.getPreview().map(Collections::singletonList)), - "", - true).orElse(Collections.emptyList()); - List attachments = PointerAttachment.forPointers(storyMessage.getFileAttachment() - .map(SignalServiceAttachment::asPointer) - .map(Collections::singletonList)); - - for (final DistributionId distributionId : distributionIds) { - RecipientId distributionRecipientId = SignalDatabase.distributionLists().getOrCreateByDistributionId(distributionId, manifest); - Recipient distributionListRecipient = Recipient.resolved(distributionRecipientId); - insertSentStoryMessage(message, distributionListRecipient, textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews, bodyRanges); - } - - if (groupId.isPresent()) { - Optional groupRecipient = SignalDatabase.recipients().getByGroupId(groupId.get()); - if (groupRecipient.isPresent()) { - insertSentStoryMessage(message, Recipient.resolved(groupRecipient.get()), textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews, bodyRanges); - } - } - - SignalDatabase.storySends().applySentStoryManifest(manifest, message.getTimestamp()); - } - - private void insertSentStoryMessage(@NonNull SentTranscriptMessage message, - @NonNull Recipient recipient, - @Nullable String textStoryBody, - @NonNull List pendingAttachments, - long sentAtTimestamp, - @NonNull StoryType storyType, - @NonNull List linkPreviews, - @Nullable BodyRangeList bodyRanges) - throws MmsException - { - if (SignalDatabase.messages().isOutgoingStoryAlreadyInDatabase(recipient.getId(), sentAtTimestamp)) { - warn(sentAtTimestamp, "Already inserted this story."); - return; - } - - OutgoingMessage mediaMessage = new OutgoingMessage(recipient, - textStoryBody, - pendingAttachments, - sentAtTimestamp, - 0, - false, - ThreadTable.DistributionTypes.DEFAULT, - storyType, - null, - false, - null, - Collections.emptyList(), - linkPreviews, - Collections.emptyList(), - Collections.emptySet(), - Collections.emptySet(), - null, - true, - bodyRanges, - -1, - 0); - - MessageTable messageTable = SignalDatabase.messages(); - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - - long messageId; - List attachments; - - messageTable.beginTransaction(); - try { - messageId = messageTable.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNDELIVERED, null); - - if (recipient.isGroup()) { - updateGroupReceiptStatus(message, messageId, recipient.requireGroupId()); - } else if (recipient.getDistributionListId().isPresent()){ - updateGroupReceiptStatusForDistributionList(message, messageId, recipient.getDistributionListId().get()); - } else { - messageTable.markUnidentified(messageId, isUnidentified(message, recipient)); - } - - messageTable.markAsSent(messageId, true); - - List allAttachments = SignalDatabase.attachments().getAttachmentsForMessage(messageId); - - attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); - - if (recipient.isSelf()) { - SignalDatabase.messages().incrementDeliveryReceiptCount(message.getTimestamp(), recipient.getId(), System.currentTimeMillis()); - SignalDatabase.messages().incrementReadReceiptCount(message.getTimestamp(), recipient.getId(), System.currentTimeMillis()); - } - - messageTable.setTransactionSuccessful(); - } finally { - messageTable.endTransaction(); - } - - for (DatabaseAttachment attachment : attachments) { - ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(messageId, attachment.getAttachmentId(), false)); - } - } - - private @NonNull StoryType getStoryType(SignalServiceStoryMessage storyMessage) { - if (storyMessage.getAllowsReplies().orElse(false)) { - if (storyMessage.getTextAttachment().isPresent()) { - return StoryType.TEXT_STORY_WITH_REPLIES; - } else { - return StoryType.STORY_WITH_REPLIES; - } - } else { - if (storyMessage.getTextAttachment().isPresent()) { - return StoryType.TEXT_STORY_WITHOUT_REPLIES; - } else { - return StoryType.STORY_WITHOUT_REPLIES; - } - } - } - - private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) - throws MmsException, BadGroupIdException - { - log(envelopeTimestamp, "Synchronize sent media message for " + message.getTimestamp()); - - MessageTable database = SignalDatabase.messages(); - Recipient recipient = getSyncMessageDestination(message); - Optional quote = getValidatedQuote(message.getDataMessage().get().getQuote()); - Optional sticker = getStickerAttachment(message.getDataMessage().get().getSticker()); - Optional> sharedContacts = getContacts(message.getDataMessage().get().getSharedContacts()); - Optional> previews = getLinkPreviews(message.getDataMessage().get().getPreviews(), message.getDataMessage().get().getBody().orElse(""), false); - Optional> mentions = getMentions(message.getDataMessage().get().getMentions()); - Optional giftBadge = getGiftBadge(message.getDataMessage().get().getGiftBadge()); - boolean viewOnce = message.getDataMessage().get().isViewOnce(); - BodyRangeList bodyRanges = getBodyRangeList(message.getDataMessage().get().getBodyRanges()); - List syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false)) - : PointerAttachment.forPointers(message.getDataMessage().get().getAttachments()); - - if (sticker.isPresent()) { - syncAttachments.add(sticker.get()); - } - - OutgoingMessage mediaMessage = new OutgoingMessage(recipient, - message.getDataMessage().get().getBody().orElse(null), - syncAttachments, - message.getTimestamp(), - TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()), - viewOnce, - ThreadTable.DistributionTypes.DEFAULT, - StoryType.NONE, - null, - false, - quote.orElse(null), - sharedContacts.orElse(Collections.emptyList()), - previews.orElse(Collections.emptyList()), - mentions.orElse(Collections.emptyList()), - Collections.emptySet(), - Collections.emptySet(), - giftBadge.orElse(null), - true, - bodyRanges, - -1, - 0); - - if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) { - handleSynchronizeSentExpirationUpdate(message); - } - - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - - long messageId; - List attachments; - List stickerAttachments; - - database.beginTransaction(); - try { - messageId = database.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null); - - if (recipient.isGroup()) { - updateGroupReceiptStatus(message, messageId, recipient.requireGroupId()); - } else { - database.markUnidentified(messageId, isUnidentified(message, recipient)); - } - - database.markAsSent(messageId, true); - - List allAttachments = SignalDatabase.attachments().getAttachmentsForMessage(messageId); - - stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); - attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); - - if (message.getDataMessage().get().getExpiresInSeconds() > 0) { - database.markExpireStarted(messageId, message.getExpirationStartTimestamp()); - ApplicationDependencies.getExpiringMessageManager() - .scheduleDeletion(messageId, - true, - message.getExpirationStartTimestamp(), - TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds())); - } - - if (recipient.isSelf()) { - SyncMessageId id = new SyncMessageId(recipient.getId(), message.getTimestamp()); - SignalDatabase.messages().incrementDeliveryReceiptCount(message.getTimestamp(), recipient.getId(), System.currentTimeMillis()); - SignalDatabase.messages().incrementReadReceiptCount(message.getTimestamp(), recipient.getId(), System.currentTimeMillis()); - } - - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - - for (DatabaseAttachment attachment : attachments) { - ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(messageId, attachment.getAttachmentId(), false)); - } - - forceStickerDownloadIfNecessary(messageId, stickerAttachments); - - return threadId; - } - - private void handleGroupRecipientUpdate(@NonNull SentTranscriptMessage message, long envelopeTimestamp) { - log(envelopeTimestamp, "Group recipient update."); - - Recipient recipient = getSyncMessageDestination(message); - - if (!recipient.isGroup()) { - warn("Got recipient update for a non-group message! Skipping."); - return; - } - - MessageRecord record = SignalDatabase.messages().getMessageFor(message.getTimestamp(), Recipient.self().getId()); - - if (record == null) { - warn("Got recipient update for non-existing message! Skipping."); - return; - } - - if (!record.isMms()) { - warn("Recipient update matched a non-MMS message! Skipping."); - return; - } - - updateGroupReceiptStatus(message, record.getId(), recipient.requireGroupId()); - } - - private void updateGroupReceiptStatus(@NonNull SentTranscriptMessage message, long messageId, @NonNull GroupId groupString) { - GroupReceiptTable receiptDatabase = SignalDatabase.groupReceipts(); - List messageRecipientIds = Stream.of(message.getRecipients()).map(RecipientId::from).toList(); - List members = SignalDatabase.groups().getGroupMembers(groupString, GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); - Map localReceipts = Stream.of(receiptDatabase.getGroupReceiptInfo(messageId)) - .collect(Collectors.toMap(GroupReceiptInfo::getRecipientId, GroupReceiptInfo::getStatus)); - - for (RecipientId messageRecipientId : messageRecipientIds) { - //noinspection ConstantConditions - if (localReceipts.containsKey(messageRecipientId) && localReceipts.get(messageRecipientId) < GroupReceiptTable.STATUS_UNDELIVERED) { - receiptDatabase.update(messageRecipientId, messageId, GroupReceiptTable.STATUS_UNDELIVERED, message.getTimestamp()); - } else if (!localReceipts.containsKey(messageRecipientId)) { - receiptDatabase.insert(Collections.singletonList(messageRecipientId), messageId, GroupReceiptTable.STATUS_UNDELIVERED, message.getTimestamp()); - } - } - - List> unidentifiedStatus = Stream.of(members) - .map(m -> new org.signal.libsignal.protocol.util.Pair<>(m.getId(), message.isUnidentified(m.requireServiceId()))) - .toList(); - receiptDatabase.setUnidentified(unidentifiedStatus, messageId); - } - - private void updateGroupReceiptStatusForDistributionList(@NonNull SentTranscriptMessage message, long messageId, @NonNull DistributionListId distributionListId) { - GroupReceiptTable receiptTable = SignalDatabase.groupReceipts(); - List messageRecipientIds = message.getRecipients().stream().map(RecipientId::from).collect(java.util.stream.Collectors.toList()); - List members = SignalDatabase.distributionLists().getMembers(distributionListId).stream().map(Recipient::resolved).collect(java.util.stream.Collectors.toList()); - Map localReceipts = receiptTable.getGroupReceiptInfo(messageId).stream().collect(java.util.stream.Collectors.toMap(GroupReceiptInfo::getRecipientId, GroupReceiptInfo::getStatus)); - - for (RecipientId messageRecipientId : messageRecipientIds) { - //noinspection ConstantConditions - if (localReceipts.containsKey(messageRecipientId) && localReceipts.get(messageRecipientId) < GroupReceiptTable.STATUS_UNDELIVERED) { - receiptTable.update(messageRecipientId, messageId, GroupReceiptTable.STATUS_UNDELIVERED, message.getTimestamp()); - } else if (!localReceipts.containsKey(messageRecipientId)) { - receiptTable.insert(Collections.singletonList(messageRecipientId), messageId, GroupReceiptTable.STATUS_UNDELIVERED, message.getTimestamp()); - } - } - - List> unidentifiedStatus = members.stream() - .map(m -> new org.signal.libsignal.protocol.util.Pair<>(m.getId(), message.isUnidentified(m.requireServiceId()))) - .collect(java.util.stream.Collectors.toList()); - receiptTable.setUnidentified(unidentifiedStatus, messageId); - } - - private @Nullable MessageId handleTextMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message, - @NonNull Optional smsMessageId, - @NonNull Optional groupId, - @NonNull Recipient senderRecipient, - @NonNull Recipient threadRecipient, - long receivedTime) - throws StorageFailedException - { - log(message.getTimestamp(), "Text message."); - MessageTable database = SignalDatabase.messages(); - String body = message.getBody().isPresent() ? message.getBody().get() : ""; - - handlePossibleExpirationUpdate(content, message, groupId, senderRecipient, threadRecipient, receivedTime); - - Optional insertResult; - - if (smsMessageId.isPresent() && !message.getGroupContext().isPresent()) { - insertResult = Optional.of(database.updateBundleMessageBody(smsMessageId.get(), body)); - } else { - notifyTypingStoppedFromIncomingMessage(senderRecipient, threadRecipient, content.getSenderDevice()); - - IncomingTextMessage textMessage = new IncomingTextMessage(senderRecipient.getId(), - content.getSenderDevice(), - message.getTimestamp(), - content.getServerReceivedTimestamp(), - receivedTime, - body, - groupId, - TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()), - content.isNeedsReceipt(), - content.getServerUuid()); - - textMessage = new IncomingEncryptedMessage(textMessage, body); - insertResult = database.insertMessageInbox(textMessage); - - if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get()); - } - - if (insertResult.isPresent()) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - return new MessageId(insertResult.get().getMessageId()); - } else { - return null; - } - } - - private long handleSynchronizeSentTextMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) - throws MmsException, BadGroupIdException - { - log(envelopeTimestamp, "Synchronize sent text message for " + message.getTimestamp()); - - Recipient recipient = getSyncMessageDestination(message); - String body = message.getDataMessage().get().getBody().orElse(""); - long expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()); - BodyRangeList bodyRanges = message.getDataMessage().get().getBodyRanges().map(DatabaseProtosUtil::toBodyRangeList).orElse(null); - - if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) { - handleSynchronizeSentExpirationUpdate(message); - } - - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - boolean isGroup = recipient.isGroup(); - - MessageTable database; - long messageId; - - if (isGroup) { - OutgoingMessage outgoingMessage = new OutgoingMessage(recipient, - new SlideDeck(), - body, - message.getTimestamp(), - expiresInMillis, - false, - StoryType.NONE, - Collections.emptyList(), - Collections.emptyList(), - true, - bodyRanges, - Collections.emptyList()); - - messageId = SignalDatabase.messages().insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null); - database = SignalDatabase.messages(); - - updateGroupReceiptStatus(message, messageId, recipient.requireGroupId()); - } else { - OutgoingMessage outgoingTextMessage = OutgoingMessage.text(recipient, body, expiresInMillis, message.getTimestamp(), bodyRanges); - - messageId = SignalDatabase.messages().insertMessageOutbox(outgoingTextMessage, threadId, false, null); - database = SignalDatabase.messages(); - database.markUnidentified(messageId, isUnidentified(message, recipient)); - } - SignalDatabase.threads().update(threadId, true); - database.markAsSent(messageId, true); - - if (expiresInMillis > 0) { - database.markExpireStarted(messageId, message.getExpirationStartTimestamp()); - ApplicationDependencies.getExpiringMessageManager() - .scheduleDeletion(messageId, isGroup, message.getExpirationStartTimestamp(), expiresInMillis); - } - - if (recipient.isSelf()) { - SignalDatabase.messages().incrementDeliveryReceiptCount(message.getTimestamp(), recipient.getId(), System.currentTimeMillis()); - SignalDatabase.messages().incrementReadReceiptCount(message.getTimestamp(), recipient.getId(), System.currentTimeMillis()); - } - - return threadId; - } - - private void handleInvalidVersionMessage(@NonNull String sender, int senderDevice, long timestamp, - @NonNull Optional smsMessageId) - { - log(timestamp, "Invalid version message."); - - MessageTable smsDatabase = SignalDatabase.messages(); - - if (!smsMessageId.isPresent()) { - Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp); - - if (insertResult.isPresent()) { - smsDatabase.markAsInvalidVersionKeyExchange(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - } - } else { - smsDatabase.markAsInvalidVersionKeyExchange(smsMessageId.get()); - } - } - - private void handleCorruptMessage(@NonNull String sender, int senderDevice, long timestamp, - @NonNull Optional smsMessageId) - { - log(timestamp, "Corrupt message."); - - MessageTable smsDatabase = SignalDatabase.messages(); - - if (!smsMessageId.isPresent()) { - Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp); - - if (insertResult.isPresent()) { - smsDatabase.markAsDecryptFailed(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - } - } else { - smsDatabase.markAsDecryptFailed(smsMessageId.get()); - } - } - - private void handleUnsupportedDataMessage(@NonNull String sender, - int senderDevice, - @NonNull Optional groupId, - long timestamp, - @NonNull Optional smsMessageId) - { - log(timestamp, "Unsupported data message."); - - MessageTable smsDatabase = SignalDatabase.messages(); - - if (!smsMessageId.isPresent()) { - Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp, groupId); - - if (insertResult.isPresent()) { - smsDatabase.markAsUnsupportedProtocolVersion(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - } - } else { - smsDatabase.markAsNoSession(smsMessageId.get()); - } - } - - private void handleInvalidMessage(@NonNull SignalServiceAddress sender, - int senderDevice, - @NonNull Optional groupId, - long timestamp, - @NonNull Optional smsMessageId) - { - log(timestamp, "Invalid message."); - - MessageTable smsDatabase = SignalDatabase.messages(); - - if (!smsMessageId.isPresent()) { - Optional insertResult = insertPlaceholder(sender.getIdentifier(), senderDevice, timestamp, groupId); - - if (insertResult.isPresent()) { - smsDatabase.markAsInvalidMessage(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - } - } else { - smsDatabase.markAsNoSession(smsMessageId.get()); - } - } - - private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp, - @NonNull Optional smsMessageId) - { - log(timestamp, "Legacy message."); - - MessageTable smsDatabase = SignalDatabase.messages(); - - if (!smsMessageId.isPresent()) { - Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp); - - if (insertResult.isPresent()) { - smsDatabase.markAsLegacyVersion(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); - } - } else { - smsDatabase.markAsLegacyVersion(smsMessageId.get()); - } - } - - private void handleProfileKey(@NonNull SignalServiceContent content, - @NonNull byte[] messageProfileKeyBytes, - @NonNull Recipient senderRecipient) - { - RecipientTable database = SignalDatabase.recipients(); - ProfileKey messageProfileKey = ProfileKeyUtil.profileKeyOrNull(messageProfileKeyBytes); - - if (senderRecipient.isSelf()) { - if (!Objects.equals(ProfileKeyUtil.getSelfProfileKey(), messageProfileKey)) { - warn(content.getTimestamp(), "Saw a sync message whose profile key doesn't match our records. Scheduling a storage sync to check."); - StorageSyncHelper.scheduleSyncForDataChange(); - } - } else if (messageProfileKey != null) { - if (database.setProfileKey(senderRecipient.getId(), messageProfileKey)) { - log(content.getTimestamp(), "Profile key on message from " + senderRecipient.getId() + " didn't match our local store. It has been updated."); - ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(senderRecipient.getId())); - } - } else { - warn(String.valueOf(content.getTimestamp()), "Ignored invalid profile key seen in message"); - } - } - - private void handleNeedsDeliveryReceipt(@NonNull RecipientId senderId, - @NonNull SignalServiceDataMessage message, - @NonNull MessageId messageId) - { - SignalExecutors.BOUNDED.execute(() -> ApplicationDependencies.getJobManager().add(new SendDeliveryReceiptJob(senderId, message.getTimestamp(), messageId))); - } - - private void handleViewedReceipt(@NonNull SignalServiceContent content, - @NonNull SignalServiceReceiptMessage message, - @NonNull Recipient senderRecipient, - boolean processingEarlyContent) - { - boolean readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context); - boolean storyViewedReceipts = SignalStore.storyValues().getViewedReceiptsEnabled(); - - if (!readReceipts && !storyViewedReceipts) { - log("Ignoring viewed receipts for IDs: " + Util.join(message.getTimestamps(), ", ")); - return; - } - - log(TAG, "Processing viewed receipts. Sender: " + senderRecipient.getId() + ", Device: " + content.getSenderDevice() + ", Only Stories: " + (!readReceipts && storyViewedReceipts) + ", Timestamps: " + Util.join(message.getTimestamps(), ", ")); - - final Set unhandled; - if (readReceipts && storyViewedReceipts) { - unhandled = SignalDatabase.messages().incrementViewedReceiptCounts(message.getTimestamps(), senderRecipient.getId(), content.getTimestamp()); - } else if (readReceipts) { - unhandled = SignalDatabase.messages().incrementViewedNonStoryReceiptCounts(message.getTimestamps(), senderRecipient.getId(), content.getTimestamp()); - } else { - unhandled = SignalDatabase.messages().incrementViewedStoryReceiptCounts(message.getTimestamps(), senderRecipient.getId(), content.getTimestamp()); - } - - Set handled = new HashSet<>(unhandled); - handled.removeAll(unhandled); - - SignalDatabase.messages().updateViewedStories(handled); - - if (unhandled.size() > 0) { - RecipientId selfId = Recipient.self().getId(); - - for (long timestamp : unhandled) { - warn(String.valueOf(content.getTimestamp()), "[handleViewedReceipt] Could not find matching message! timestamp: " + timestamp + ", author: " + senderRecipient.getId() + " | Receipt so associating with message from self (" + selfId + ")"); - if (!processingEarlyContent) { - ApplicationDependencies.getEarlyMessageCache().store(selfId, timestamp, content); - } - } - } - - if (unhandled.size() > 0 && !processingEarlyContent) { - PushProcessEarlyMessagesJob.enqueue(); - } - } - - @SuppressLint("DefaultLocale") - private void handleDeliveryReceipt(@NonNull SignalServiceContent content, - @NonNull SignalServiceReceiptMessage message, - @NonNull Recipient senderRecipient) - { - log(content.getTimestamp(), "Processing delivery receipts. Sender: " + senderRecipient.getId() + ", Device: " + content.getSenderDevice() + ", Timestamps: " + Util.join(message.getTimestamps(), ", ")); - - Set unhandled = SignalDatabase.messages().incrementDeliveryReceiptCounts(message.getTimestamps(), senderRecipient.getId(), content.getTimestamp()); - - for (long timestamp : unhandled) { - warn(String.valueOf(content.getTimestamp()), "[handleDeliveryReceipt] Could not find matching message! timestamp: " + timestamp + " author: " + senderRecipient.getId()); - // Early delivery receipts are special-cased in the database methods - } - - if (unhandled.size() > 0) { - PushProcessEarlyMessagesJob.enqueue(); - } - - SignalDatabase.pendingPniSignatureMessages().acknowledgeReceipts(senderRecipient.getId(), message.getTimestamps(), content.getSenderDevice()); - SignalDatabase.messageLog().deleteEntriesForRecipient(message.getTimestamps(), senderRecipient.getId(), content.getSenderDevice()); - } - - @SuppressLint("DefaultLocale") - private void handleReadReceipt(@NonNull SignalServiceContent content, - @NonNull SignalServiceReceiptMessage message, - @NonNull Recipient senderRecipient, - boolean processingEarlyContent) - { - if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { - log("Ignoring read receipts for IDs: " + Util.join(message.getTimestamps(), ", ")); - return; - } - - log(TAG, "Processing read receipts. Sender: " + senderRecipient.getId() + ", Device: " + content.getSenderDevice() + ", Timestamps: " + Util.join(message.getTimestamps(), ", ")); - - Set unhandled = SignalDatabase.messages().incrementReadReceiptCounts(message.getTimestamps(), senderRecipient.getId(), content.getTimestamp()); - - if (unhandled.size() > 0) { - RecipientId selfId = Recipient.self().getId(); - - for (long timestamp : unhandled) { - warn(String.valueOf(content.getTimestamp()), "[handleReadReceipt] Could not find matching message! timestamp: " + timestamp + ", author: " + senderRecipient.getId() + " | Receipt, so associating with message from self (" + selfId + ")"); - if (!processingEarlyContent) { - ApplicationDependencies.getEarlyMessageCache().store(selfId, timestamp, content); - } - } - } - - if (unhandled.size() > 0 && !processingEarlyContent) { - PushProcessEarlyMessagesJob.enqueue(); - } - } - - private void handleTypingMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceTypingMessage typingMessage, - @NonNull Recipient senderRecipient) - throws BadGroupIdException - { - if (!TextSecurePreferences.isTypingIndicatorsEnabled(context)) { - return; - } - - long threadId; - - if (typingMessage.getGroupId().isPresent()) { - GroupId.Push groupId = GroupId.push(typingMessage.getGroupId().get()); - - if (!SignalDatabase.groups().isCurrentMember(groupId, senderRecipient.getId())) { - warn(String.valueOf(content.getTimestamp()), "Seen typing indicator for non-member " + senderRecipient.getId()); - return; - } - - Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(groupId); - - threadId = SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient); - } else { - threadId = SignalDatabase.threads().getOrCreateThreadIdFor(senderRecipient); - } - - if (threadId <= 0) { - warn(String.valueOf(content.getTimestamp()), "Couldn't find a matching thread for a typing message."); - return; - } - - if (typingMessage.isTypingStarted()) { - Log.d(TAG, "Typing started on thread " + threadId); - ApplicationDependencies.getTypingStatusRepository().onTypingStarted(context,threadId, senderRecipient, content.getSenderDevice()); - } else { - Log.d(TAG, "Typing stopped on thread " + threadId); - ApplicationDependencies.getTypingStatusRepository().onTypingStopped(threadId, senderRecipient, content.getSenderDevice(), false); - } - } - - private void handleRetryReceipt(@NonNull SignalServiceContent content, @NonNull DecryptionErrorMessage decryptionErrorMessage, @NonNull Recipient senderRecipient) { - if (!FeatureFlags.retryReceipts()) { - warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Feature flag disabled, skipping retry receipt."); - return; - } - - if (decryptionErrorMessage.getDeviceId() != SignalStore.account().getDeviceId()) { - log(String.valueOf(content.getTimestamp()), "[RetryReceipt] Received a DecryptionErrorMessage targeting a linked device. Ignoring."); - return; - } - - long sentTimestamp = decryptionErrorMessage.getTimestamp(); - - warn(content.getTimestamp(), "[RetryReceipt] Received a retry receipt from " + formatSender(senderRecipient, content) + " for message with timestamp " + sentTimestamp + "."); - - if (!senderRecipient.hasServiceId()) { - warn(content.getTimestamp(), "[RetryReceipt] Requester " + senderRecipient.getId() + " somehow has no UUID! timestamp: " + sentTimestamp); - return; - } - - MessageLogEntry messageLogEntry = SignalDatabase.messageLog().getLogEntry(senderRecipient.getId(), content.getSenderDevice(), sentTimestamp); - - if (decryptionErrorMessage.getRatchetKey().isPresent()) { - handleIndividualRetryReceipt(senderRecipient, messageLogEntry, content, decryptionErrorMessage); - } else { - handleSenderKeyRetryReceipt(senderRecipient, messageLogEntry, content, decryptionErrorMessage); - } - } - - private void handleSenderKeyRetryReceipt(@NonNull Recipient requester, - @Nullable MessageLogEntry messageLogEntry, - @NonNull SignalServiceContent content, - @NonNull DecryptionErrorMessage decryptionErrorMessage) - { - long sentTimestamp = decryptionErrorMessage.getTimestamp(); - MessageRecord relatedMessage = findRetryReceiptRelatedMessage(context, messageLogEntry, sentTimestamp); - - if (relatedMessage == null) { - warn(content.getTimestamp(), "[RetryReceipt-SK] The related message could not be found! There shouldn't be any sender key resends where we can't find the related message. Skipping."); - return; - } - - Recipient threadRecipient = SignalDatabase.threads().getRecipientForThreadId(relatedMessage.getThreadId()); - if (threadRecipient == null) { - warn(content.getTimestamp(), "[RetryReceipt-SK] Could not find a thread recipient! Skipping."); - return; - } - - if (!threadRecipient.isPushV2Group() && !threadRecipient.isDistributionList()) { - warn(content.getTimestamp(), "[RetryReceipt-SK] Thread recipient is not a V2 group or distribution list! Skipping."); - return; - } - - DistributionId distributionId; - GroupId.V2 groupId; - - if (threadRecipient.isGroup()) { - groupId = threadRecipient.requireGroupId().requireV2(); - distributionId = SignalDatabase.groups().getOrCreateDistributionId(groupId); - } else { - groupId = null; - distributionId = SignalDatabase.distributionLists().getDistributionId(threadRecipient.getId()); - } - - if (distributionId == null) { - Log.w(TAG, "[RetryReceipt-SK] Failed to find a distributionId! Skipping."); - return; - } - - SignalProtocolAddress requesterAddress = new SignalProtocolAddress(requester.requireServiceId().toString(), content.getSenderDevice()); - - SignalDatabase.senderKeyShared().delete(distributionId, Collections.singleton(requesterAddress)); - - if (messageLogEntry != null) { - warn(content.getTimestamp(), "[RetryReceipt-SK] Found MSL entry for " + requester.getId() + " (" + requesterAddress + ") with timestamp " + sentTimestamp + ". Scheduling a resend."); - - ApplicationDependencies.getJobManager().add(new ResendMessageJob(messageLogEntry.getRecipientId(), - messageLogEntry.getDateSent(), - messageLogEntry.getContent(), - messageLogEntry.getContentHint(), - messageLogEntry.isUrgent(), - groupId, - distributionId)); - } else { - warn(content.getTimestamp(), "[RetryReceipt-SK] Unable to find MSL entry for " + requester.getId() + " (" + requesterAddress + ") with timestamp " + sentTimestamp + " for " + (groupId != null ? "group " + groupId : "distribution list") + ". Scheduling a job to send them the SenderKeyDistributionMessage. Membership will be checked there."); - ApplicationDependencies.getJobManager().add(new SenderKeyDistributionSendJob(requester.getId(), threadRecipient.getId())); - } - } - - private void handleIndividualRetryReceipt(@NonNull Recipient requester, @Nullable MessageLogEntry messageLogEntry, @NonNull SignalServiceContent content, @NonNull DecryptionErrorMessage decryptionErrorMessage) { - boolean archivedSession = false; - - // TODO [pnp] Ignore retry receipts that have a PNI destinationUuid - - if (decryptionErrorMessage.getRatchetKey().isPresent() && - ratchetKeyMatches(requester, content.getSenderDevice(), decryptionErrorMessage.getRatchetKey().get())) - { - warn(content.getTimestamp(), "[RetryReceipt-I] Ratchet key matches. Archiving the session."); - ApplicationDependencies.getProtocolStore().aci().sessions().archiveSession(requester.requireServiceId(), content.getSenderDevice()); - archivedSession = true; - } - - if (messageLogEntry != null) { - warn(content.getTimestamp(), "[RetryReceipt-I] Found an entry in the MSL. Resending."); - ApplicationDependencies.getJobManager().add(new ResendMessageJob(messageLogEntry.getRecipientId(), - messageLogEntry.getDateSent(), - messageLogEntry.getContent(), - messageLogEntry.getContentHint(), - messageLogEntry.isUrgent(), - null, - null)); - } else if (archivedSession) { - warn(content.getTimestamp(), "[RetryReceipt-I] Could not find an entry in the MSL, but we archived the session, so we're sending a null message to complete the reset."); - ApplicationDependencies.getJobManager().add(new NullMessageSendJob(requester.getId())); - } else { - warn(content.getTimestamp(), "[RetryReceipt-I] Could not find an entry in the MSL. Skipping."); - } - } - - private @Nullable MessageRecord findRetryReceiptRelatedMessage(@NonNull Context context, @Nullable MessageLogEntry messageLogEntry, long sentTimestamp) { - if (messageLogEntry != null && messageLogEntry.hasRelatedMessage()) { - MessageId relatedMessage = messageLogEntry.getRelatedMessages().get(0); - return SignalDatabase.messages().getMessageRecordOrNull(relatedMessage.getId()); - } else { - return SignalDatabase.messages().getMessageFor(sentTimestamp, Recipient.self().getId()); - } - } - - public static boolean ratchetKeyMatches(@NonNull Recipient recipient, int deviceId, @NonNull ECPublicKey ratchetKey) { - SignalProtocolAddress address = recipient.resolve().requireServiceId().toProtocolAddress(deviceId); - SessionRecord session = ApplicationDependencies.getProtocolStore().aci().loadSession(address); - - return session.currentRatchetKeyMatches(ratchetKey); - } - - private static boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) { - if (message.isViewOnce()) { - List attachments = message.getAttachments().orElse(Collections.emptyList()); - - return attachments.size() != 1 || - !isViewOnceSupportedContentType(attachments.get(0).getContentType().toLowerCase()); - } - - return false; - } - - private static boolean isViewOnceSupportedContentType(@NonNull String contentType) { - return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType); - } - - private Optional getValidatedQuote(Optional quote) { - if (!quote.isPresent()) return Optional.empty(); - - if (quote.get().getId() <= 0) { - warn("Received quote without an ID! Ignoring..."); - return Optional.empty(); - } - - if (quote.get().getAuthor() == null) { - warn("Received quote without an author! Ignoring..."); - return Optional.empty(); - } - - RecipientId author = Recipient.externalPush(quote.get().getAuthor()).getId(); - MessageRecord message = SignalDatabase.messages().getMessageFor(quote.get().getId(), author); - - if (message != null && !message.isRemoteDelete()) { - log("Found matching message record..."); - - List attachments = new LinkedList<>(); - List mentions = new LinkedList<>(); - - if (message.isMms()) { - MmsMessageRecord mmsMessage = (MmsMessageRecord) message; - - if (mmsMessage instanceof MediaMmsMessageRecord) { - mmsMessage = ((MediaMmsMessageRecord) mmsMessage).withAttachments(context, SignalDatabase.attachments().getAttachmentsForMessage(mmsMessage.getId())); - } - - mentions.addAll(SignalDatabase.mentions().getMentionsForMessage(mmsMessage.getId())); - - if (mmsMessage.isViewOnce()) { - attachments.add(new TombstoneAttachment(MediaUtil.VIEW_ONCE, true)); - } else { - attachments = mmsMessage.getSlideDeck().asAttachments(); - - if (attachments.isEmpty()) { - attachments.addAll(Stream.of(mmsMessage.getLinkPreviews()) - .filter(lp -> lp.getThumbnail().isPresent()) - .map(lp -> lp.getThumbnail().get()) - .toList()); - } - } - - if (message.isPaymentNotification()) { - message = SignalDatabase.payments().updateMessageWithPayment(message); - } - } - - String body = message.isPaymentNotification() ? message.getDisplayBody(context).toString() : message.getBody(); - - return Optional.of(new QuoteModel(quote.get().getId(), author, body, false, attachments, mentions, QuoteModel.Type.fromDataMessageType(quote.get().getType()), message.getMessageRanges())); - } else if (message != null) { - warn("Found the target for the quote, but it's flagged as remotely deleted."); - } - - warn("Didn't find matching message record..."); - - return Optional.of(new QuoteModel(quote.get().getId(), - author, - quote.get().getText(), - true, - PointerAttachment.forPointers(quote.get().getAttachments()), - getMentions(quote.get().getMentions()), - QuoteModel.Type.fromDataMessageType(quote.get().getType()), - DatabaseProtosUtil.toBodyRangeList(quote.get().getBodyRanges()))); - } - - private Optional getStickerAttachment(Optional sticker) { - if (!sticker.isPresent()) { - return Optional.empty(); - } - - if (sticker.get().getPackId() == null || sticker.get().getPackKey() == null || sticker.get().getAttachment() == null) { - warn("Malformed sticker!"); - return Optional.empty(); - } - - String packId = Hex.toStringCondensed(sticker.get().getPackId()); - String packKey = Hex.toStringCondensed(sticker.get().getPackKey()); - int stickerId = sticker.get().getStickerId(); - String emoji = sticker.get().getEmoji(); - StickerLocator stickerLocator = new StickerLocator(packId, packKey, stickerId, emoji); - StickerTable stickerDatabase = SignalDatabase.stickers(); - StickerRecord stickerRecord = stickerDatabase.getSticker(stickerLocator.getPackId(), stickerLocator.getStickerId(), false); - - if (stickerRecord != null) { - return Optional.of(new UriAttachment(stickerRecord.getUri(), - stickerRecord.getContentType(), - AttachmentTable.TRANSFER_PROGRESS_DONE, - stickerRecord.getSize(), - StickerSlide.WIDTH, - StickerSlide.HEIGHT, - null, - String.valueOf(new SecureRandom().nextLong()), - false, - false, - false, - false, - null, - stickerLocator, - null, - null, - null)); - } else { - return Optional.of(PointerAttachment.forPointer(Optional.of(sticker.get().getAttachment()), stickerLocator).get()); - } - } - - private static Optional> getContacts(Optional> sharedContacts) { - if (!sharedContacts.isPresent()) return Optional.empty(); - - List contacts = new ArrayList<>(sharedContacts.get().size()); - - for (SharedContact sharedContact : sharedContacts.get()) { - contacts.add(ContactModelMapper.remoteToLocal(sharedContact)); - } - - return Optional.of(contacts); - } - - private Optional> getLinkPreviews(Optional> previews, @NonNull String message, boolean isStoryEmbed) { - if (!previews.isPresent() || previews.get().isEmpty()) return Optional.empty(); - - List linkPreviews = new ArrayList<>(previews.get().size()); - LinkPreviewUtil.Links urlsInMessage = LinkPreviewUtil.findValidPreviewUrls(message); - - for (SignalServicePreview preview : previews.get()) { - Optional thumbnail = PointerAttachment.forPointer(preview.getImage()); - Optional url = Optional.ofNullable(preview.getUrl()); - Optional title = Optional.ofNullable(preview.getTitle()); - Optional description = Optional.ofNullable(preview.getDescription()); - boolean hasTitle = !TextUtils.isEmpty(title.orElse("")); - boolean presentInBody = url.isPresent() && urlsInMessage.containsUrl(url.get()); - boolean validDomain = url.isPresent() && LinkUtil.isValidPreviewUrl(url.get()); - - if (hasTitle && (presentInBody || isStoryEmbed) && validDomain) { - LinkPreview linkPreview = new LinkPreview(url.get(), title.orElse(""), description.orElse(""), preview.getDate(), thumbnail); - linkPreviews.add(linkPreview); - } else { - warn(String.format("Discarding an invalid link preview. hasTitle: %b presentInBody: %b isStoryEmbed: %b validDomain: %b", hasTitle, presentInBody, isStoryEmbed, validDomain)); - } - } - - return Optional.of(linkPreviews); - } - - private Optional> getMentions(Optional> signalServiceMentions) { - if (!signalServiceMentions.isPresent()) return Optional.empty(); - - return Optional.of(getMentions(signalServiceMentions.get())); - } - - private @Nullable BodyRangeList getBodyRangeList(Optional> bodyRanges) { - if (bodyRanges.isEmpty()) { - return null; - } - - return DatabaseProtosUtil.toBodyRangeList(bodyRanges.get()); - } - - private @NonNull List getMentions(@Nullable List signalServiceMentions) { - if (signalServiceMentions == null || signalServiceMentions.isEmpty()) { - return Collections.emptyList(); - } - - List mentions = new ArrayList<>(signalServiceMentions.size()); - - for (SignalServiceDataMessage.Mention mention : signalServiceMentions) { - if (!mention.getServiceId().isUnknown()) { - mentions.add(new Mention(Recipient.externalPush(mention.getServiceId()).getId(), mention.getStart(), mention.getLength())); - } - } - - return mentions; - } - - private Optional getGiftBadge(Optional giftBadge) { - if (!giftBadge.isPresent()) return Optional.empty(); - - return Optional.of(GiftBadge.newBuilder() - .setRedemptionToken(ByteString.copyFrom(giftBadge.get().getReceiptCredentialPresentation().serialize())) - .build()); - } - - private Optional insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) { - return insertPlaceholder(sender, senderDevice, timestamp, Optional.empty()); - } - - private Optional insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional groupId) { - MessageTable database = SignalDatabase.messages(); - IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.external(context, sender).getId(), - senderDevice, timestamp, -1, System.currentTimeMillis(), "", - groupId, 0, false, null); - - textMessage = new IncomingEncryptedMessage(textMessage, ""); - return database.insertMessageInbox(textMessage); - } - - private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message) { - if (message.getDataMessage().get().getGroupContext().isPresent()) { - return Recipient.externalPossiblyMigratedGroup(GroupId.v2(message.getDataMessage().get().getGroupContext().get().getMasterKey())); - } else { - return Recipient.externalPush(message.getDestination().get()); - } - } - - private Recipient getMessageDestination(@NonNull SignalServiceContent content, @NonNull Recipient sender) throws BadGroupIdException { - if (content.getStoryMessage().isPresent()) { - SignalServiceStoryMessage message = content.getStoryMessage().get(); - return getGroupRecipient(message.getGroupContext(), sender); - } else { - SignalServiceDataMessage message = content.getDataMessage().orElse(null); - return getGroupRecipient(message != null ? message.getGroupContext() : Optional.empty(), sender); - } - } - - private @NonNull Recipient getGroupRecipient(Optional signalServiceGroup, @NonNull Recipient senderRecipient) { - if (signalServiceGroup.isPresent()) { - return Recipient.externalPossiblyMigratedGroup(GroupId.v2(signalServiceGroup.get().getMasterKey())); - } else { - return senderRecipient; - } - } - - private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient senderRecipient, @NonNull Recipient conversationRecipient, int device) { - long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(conversationRecipient.getId()); - - if (threadId > 0 && TextSecurePreferences.isTypingIndicatorsEnabled(context)) { - Log.d(TAG, "Typing stopped on thread " + threadId + " due to an incoming message."); - ApplicationDependencies.getTypingStatusRepository().onTypingStopped(threadId, senderRecipient, device, true); - } - } - - private boolean shouldIgnore(@NonNull SignalServiceContent content, @NonNull Recipient sender, @NonNull Recipient conversation) - throws BadGroupIdException - { - if (content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); - - if (conversation.isGroup() && conversation.isBlocked()) { - return true; - } else if (conversation.isGroup()) { - GroupTable groupDatabase = SignalDatabase.groups(); - Optional groupId = GroupUtil.idFromGroupContext(message.getGroupContext()); - - if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { - return sender.isBlocked(); - } - - boolean isTextMessage = message.getBody().isPresent(); - boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getSticker().isPresent(); - boolean isExpireMessage = message.isExpirationUpdate(); - boolean isGv2Update = message.isGroupV2Update(); - boolean isContentMessage = !isGv2Update && !isExpireMessage && (isTextMessage || isMediaMessage); - boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get()); - - return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isGv2Update); - } else { - return sender.isBlocked(); - } - } else if (content.getCallMessage().isPresent()) { - return sender.isBlocked(); - } else if (content.getTypingMessage().isPresent()) { - if (sender.isBlocked()) { - return true; - } - - if (content.getTypingMessage().get().getGroupId().isPresent()) { - GroupId groupId = GroupId.push(content.getTypingMessage().get().getGroupId().get()); - Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(groupId); - - if (groupRecipient.isBlocked() || !groupRecipient.isActiveGroup()) { - return true; - } else { - Optional groupRecord = SignalDatabase.groups().getGroup(groupId); - return groupRecord.isPresent() && groupRecord.get().isAnnouncementGroup() && !groupRecord.get().getAdmins().contains(sender); - } - } - } else if (content.getStoryMessage().isPresent()) { - if (conversation.isGroup() && conversation.isBlocked()) { - return true; - } else { - return sender.isBlocked(); - } - } - - return false; - } - - private void forceStickerDownloadIfNecessary(long messageId, List stickerAttachments) { - if (stickerAttachments.isEmpty()) return; - - DatabaseAttachment stickerAttachment = stickerAttachments.get(0); - - if (stickerAttachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE) { - AttachmentDownloadJob downloadJob = new AttachmentDownloadJob(messageId, stickerAttachment.getAttachmentId(), true); - - try { - downloadJob.setContext(context); - downloadJob.doWork(); - } catch (Exception e) { - warn("Failed to download sticker inline. Scheduling."); - ApplicationDependencies.getJobManager().add(downloadJob); - } - } - } - - private static boolean isUnidentified(@NonNull SentTranscriptMessage message, @NonNull Recipient recipient) { - if (recipient.hasServiceId()) { - return message.isUnidentified(recipient.requireServiceId()); - } else { - return false; - } - } - - private static void log(@NonNull String message) { - Log.i(TAG, message); - } - - private static void log(long timestamp, @NonNull String message) { - log(String.valueOf(timestamp), message); - } - - private static void log(@NonNull String extra, @NonNull String message) { - String extraLog = Util.isEmpty(extra) ? "" : "[" + extra + "] "; - Log.i(TAG, extraLog + message); - } - - private static void warn(@NonNull String message) { - warn("", message, null); - } - - private static void warn(@NonNull String extra, @NonNull String message) { - warn(extra, message, null); - } - - private static void warn(long timestamp, @NonNull String message) { - warn(String.valueOf(timestamp), message); - } - - private static void warn(long timestamp, @NonNull String message, @Nullable Throwable t) { - warn(String.valueOf(timestamp), message, t); - } - - private static void warn(@NonNull String message, @Nullable Throwable t) { - warn("", message, t); - } - - private static void warn(@NonNull String extra, @NonNull String message, @Nullable Throwable t) { - String extraLog = Util.isEmpty(extra) ? "" : "[" + extra + "] "; - Log.w(TAG, extraLog + message, t); - } - - private static String formatSender(@NonNull Recipient recipient, @Nullable SignalServiceContent content) { - return formatSender(recipient.getId(), content); - } - - private static String formatSender(@NonNull RecipientId recipientId, @Nullable SignalServiceContent content) { - if (content != null) { - return recipientId + " (" + content.getSender().getIdentifier() + "." + content.getSenderDevice() + ")"; - } else { - return recipientId.toString(); - } - } - - static class StorageFailedException extends Exception { - private final String sender; - private final int senderDevice; - - StorageFailedException(Exception e, String sender, int senderDevice) { - super(e); - this.sender = sender; - this.senderDevice = senderDevice; - } - - public String getSender() { - return sender; - } - - public int getSenderDevice() { - return senderDevice; - } - } - - public enum MessageState { - DECRYPTED_OK, - INVALID_VERSION, - CORRUPT_MESSAGE, // Not used, but can't remove due to serialization - NO_SESSION, // Not used, but can't remove due to serialization - LEGACY_MESSAGE, - DUPLICATE_MESSAGE, - UNSUPPORTED_DATA_MESSAGE, - NOOP, - DECRYPTION_ERROR - } - - public static final class ExceptionMetadata { - @NonNull private final String sender; - private final int senderDevice; - @Nullable private final GroupId groupId; - - public ExceptionMetadata(@NonNull String sender, int senderDevice, @Nullable GroupId groupId) { - this.sender = sender; - this.senderDevice = senderDevice; - this.groupId = groupId; - } - - public ExceptionMetadata(@NonNull String sender, int senderDevice) { - this(sender, senderDevice, null); - } - - @NonNull - public String getSender() { - return sender; - } - - public int getSenderDevice() { - return senderDevice; - } - - @Nullable - public GroupId getGroupId() { - return groupId; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessorV2.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessorV2.kt index 42e13f1de9..bbd5e46adc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessorV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessorV2.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.messages import android.content.Context import org.signal.core.util.logging.Log import org.signal.core.util.orNull +import org.signal.core.util.toOptional import org.signal.libsignal.protocol.SignalProtocolAddress import org.signal.libsignal.protocol.ecc.ECPublicKey import org.signal.libsignal.protocol.message.DecryptionErrorMessage @@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.groups.GroupNotAMemberException import org.thoughtcrime.securesms.groups.GroupsV1MigratedCache import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor +import org.thoughtcrime.securesms.jobs.AutomaticSessionResetJob import org.thoughtcrime.securesms.jobs.NullMessageSendJob import org.thoughtcrime.securesms.jobs.ResendMessageJob import org.thoughtcrime.securesms.jobs.SenderKeyDistributionSendJob @@ -38,6 +40,8 @@ import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.toDecryptionEr import org.thoughtcrime.securesms.notifications.v2.ConversationId import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage +import org.thoughtcrime.securesms.sms.IncomingTextMessage import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.SignalLocalMetrics @@ -284,6 +288,29 @@ open class MessageContentProcessorV2(private val context: Context) { null } } + + private fun insertErrorMessage(context: Context, sender: Recipient, senderDevice: Int, timestamp: Long, groupId: Optional, marker: (Long) -> Unit) { + val textMessage = IncomingTextMessage( + sender.id, + senderDevice, + timestamp, + -1, + System.currentTimeMillis(), + "", + groupId, + 0, + false, + null + ) + + SignalDatabase + .messages + .insertMessageInbox(IncomingEncryptedMessage(textMessage, "")) + .ifPresent { + marker(it.messageId) + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(it.threadId)) + } + } } /** @@ -304,7 +331,7 @@ open class MessageContentProcessorV2(private val context: Context) { val earlyCacheEntries: List? = ApplicationDependencies .getEarlyMessageCache() - .retrieveV2(senderRecipient.id, envelope.timestamp) + .retrieve(senderRecipient.id, envelope.timestamp) .orNull() if (!processingEarlyContent && earlyCacheEntries != null) { @@ -315,6 +342,63 @@ open class MessageContentProcessorV2(private val context: Context) { } } + fun processException(messageState: MessageState, exceptionMetadata: ExceptionMetadata, timestamp: Long) { + val sender = Recipient.external(context, exceptionMetadata.sender) + + if (sender.isBlocked) { + warn("Ignoring exception content from blocked sender, message state: $messageState") + return + } + + when (messageState) { + MessageState.DECRYPTION_ERROR -> { + warn(timestamp, "Handling encryption error.") + + val threadRecipient = if (exceptionMetadata.groupId != null) Recipient.externalPossiblyMigratedGroup(exceptionMetadata.groupId) else sender + SignalDatabase + .messages + .insertBadDecryptMessage( + recipientId = sender.id, + senderDevice = exceptionMetadata.senderDevice, + sentTimestamp = timestamp, + receivedTimestamp = System.currentTimeMillis(), + threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient) + ) + } + + MessageState.INVALID_VERSION -> { + warn(timestamp, "Handling invalid version.") + insertErrorMessage(context, sender, exceptionMetadata.senderDevice, timestamp, exceptionMetadata.groupId.toOptional()) { messageId -> + SignalDatabase.messages.markAsInvalidVersionKeyExchange(messageId) + } + } + + MessageState.LEGACY_MESSAGE -> { + warn(timestamp, "Handling legacy message.") + insertErrorMessage(context, sender, exceptionMetadata.senderDevice, timestamp, exceptionMetadata.groupId.toOptional()) { messageId -> + SignalDatabase.messages.markAsLegacyVersion(messageId) + } + } + + MessageState.UNSUPPORTED_DATA_MESSAGE -> { + warn(timestamp, "Handling unsupported data message.") + insertErrorMessage(context, sender, exceptionMetadata.senderDevice, timestamp, exceptionMetadata.groupId.toOptional()) { messageId -> + SignalDatabase.messages.markAsUnsupportedProtocolVersion(messageId) + } + } + + MessageState.CORRUPT_MESSAGE, + MessageState.NO_SESSION -> { + warn(timestamp, "Discovered old enqueued bad encrypted message. Scheduling reset.") + ApplicationDependencies.getJobManager().add(AutomaticSessionResetJob(sender.id, exceptionMetadata.senderDevice, timestamp)) + } + + MessageState.DUPLICATE_MESSAGE -> warn(timestamp, "Duplicate message. Dropping.") + + else -> throw AssertionError("Not handled $messageState. ($timestamp)") + } + } + private fun handleMessage( senderRecipient: Recipient, envelope: Envelope, @@ -350,6 +434,7 @@ open class MessageContentProcessorV2(private val context: Context) { localMetric ) } + content.hasSyncMessage() -> { TextSecurePreferences.setMultiDevice(context, true) @@ -362,6 +447,7 @@ open class MessageContentProcessorV2(private val context: Context) { if (processingEarlyContent) null else EarlyMessageCacheEntry(envelope, content, metadata, serverDeliveredTimestamp) ) } + content.hasCallMessage() -> { log(envelope.timestamp, "Got call message...") @@ -375,6 +461,7 @@ open class MessageContentProcessorV2(private val context: Context) { CallMessageProcessor.process(senderRecipient, envelope, content, metadata, serverDeliveredTimestamp) } + content.hasReceiptMessage() -> { ReceiptMessageProcessor.process( context, @@ -385,9 +472,11 @@ open class MessageContentProcessorV2(private val context: Context) { if (processingEarlyContent) null else EarlyMessageCacheEntry(envelope, content, metadata, serverDeliveredTimestamp) ) } + content.hasTypingMessage() -> { handleTypingMessage(envelope, metadata, content.typingMessage, senderRecipient) } + content.hasStoryMessage() -> { StoryMessageProcessor.process( envelope, @@ -397,9 +486,11 @@ open class MessageContentProcessorV2(private val context: Context) { threadRecipient ) } + content.hasDecryptionErrorMessage() -> { handleRetryReceipt(envelope, metadata, content.decryptionErrorMessage!!.toDecryptionErrorMessage(metadata), senderRecipient) } + content.hasEditMessage() -> { EditMessageProcessor.process( context, @@ -411,9 +502,11 @@ open class MessageContentProcessorV2(private val context: Context) { if (processingEarlyContent) null else EarlyMessageCacheEntry(envelope, content, metadata, serverDeliveredTimestamp) ) } + content.hasSenderKeyDistributionMessage() || content.hasPniSignatureMessage() -> { // Already handled, here in order to prevent unrecognized message log } + else -> { warn(envelope.timestamp, "Got unrecognized message!") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageState.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageState.java new file mode 100644 index 0000000000..d4c4469162 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageState.java @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.messages; + +/** + * Message processing state/result + */ +public enum MessageState { + DECRYPTED_OK, + INVALID_VERSION, + CORRUPT_MESSAGE, // Not used, but can't remove due to serialization + NO_SESSION, // Not used, but can't remove due to serialization + LEGACY_MESSAGE, + DUPLICATE_MESSAGE, + UNSUPPORTED_DATA_MESSAGE, + NOOP, + DECRYPTION_ERROR +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/StorageFailedException.java b/app/src/main/java/org/thoughtcrime/securesms/messages/StorageFailedException.java new file mode 100644 index 0000000000..7c7a7582c8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/StorageFailedException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.messages; + +class StorageFailedException extends Exception { + private final String sender; + private final int senderDevice; + + StorageFailedException(Exception e, String sender, int senderDevice) { + super(e); + this.sender = sender; + this.senderDevice = senderDevice; + } + + public String getSender() { + return sender; + } + + public int getSenderDevice() { + return senderDevice; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/StoryMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/StoryMessageProcessor.kt index 3dbf3710ea..d80b2e355f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/StoryMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/StoryMessageProcessor.kt @@ -21,10 +21,11 @@ import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.FeatureFlags import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata import org.whispersystems.signalservice.internal.push.SignalServiceProtos +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope object StoryMessageProcessor { - fun process(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, senderRecipient: Recipient, threadRecipient: Recipient) { + fun process(envelope: Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, senderRecipient: Recipient, threadRecipient: Recipient) { val storyMessage = content.storyMessage log(envelope.timestamp, "Story message.") @@ -79,7 +80,7 @@ object StoryMessageProcessor { SignalDatabase.messages.setTransactionSuccessful() } } catch (e: MmsException) { - throw MessageContentProcessor.StorageFailedException(e, metadata.sourceServiceId.toString(), metadata.sourceDeviceId) + throw StorageFailedException(e, metadata.sourceServiceId.toString(), metadata.sourceDeviceId) } finally { SignalDatabase.messages.endTransaction() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index 21fb02da1d..594fb4482a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -52,7 +52,6 @@ import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.linkpreview.LinkPreview -import org.thoughtcrime.securesms.messages.MessageContentProcessor.StorageFailedException import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.log import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.warn import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.groupId diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/WebSocketDrainer.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/WebSocketDrainer.kt index 231eda8c9d..6f834d58ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/WebSocketDrainer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/WebSocketDrainer.kt @@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.JobTracker import org.thoughtcrime.securesms.jobmanager.JobTracker.JobListener import org.thoughtcrime.securesms.jobs.MarkerJob -import org.thoughtcrime.securesms.jobs.PushProcessMessageJob +import org.thoughtcrime.securesms.jobs.PushProcessMessageJobV2 import org.thoughtcrime.securesms.util.NetworkUtil import org.thoughtcrime.securesms.util.PowerManagerCompat import org.thoughtcrime.securesms.util.ServiceUtil @@ -81,7 +81,7 @@ object WebSocketDrainer { val queueListener = QueueFindingJobListener() jobManager.addListener( - { job: Job -> job.parameters.queue?.startsWith(PushProcessMessageJob.QUEUE_PREFIX) ?: false }, + { job: Job -> job.parameters.queue?.startsWith(PushProcessMessageJobV2.QUEUE_PREFIX) ?: false }, queueListener ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 7b1e526181..79d97cdb47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -121,7 +121,7 @@ public class ApplicationMigrations { static final int GLIDE_CACHE_CLEAR = 77; static final int SYSTEM_NAME_RESYNC = 78; static final int RECOVERY_PASSWORD_SYNC = 79; - static final int DECRYPTIONS_DRAINED = 80; +// static final int DECRYPTIONS_DRAINED = 80; static final int REBUILD_MESSAGE_FTS_INDEX_3 = 81; static final int TO_FROM_RECIPIENTS = 82; static final int REBUILD_MESSAGE_FTS_INDEX_4 = 83; @@ -556,9 +556,10 @@ public class ApplicationMigrations { jobs.put(Version.RECOVERY_PASSWORD_SYNC, new AttributesMigrationJob()); } - if (lastSeenVersion < Version.DECRYPTIONS_DRAINED) { - jobs.put(Version.DECRYPTIONS_DRAINED, new DecryptionsDrainedMigrationJob()); - } + // Needed for the conversion to inline decryptions and is no longer necessary + // if (lastSeenVersion < Version.DECRYPTIONS_DRAINED) { + // jobs.put(Version.DECRYPTIONS_DRAINED, new DecryptionsDrainedMigrationJob()); + // } if (lastSeenVersion < Version.REBUILD_MESSAGE_FTS_INDEX_3) { jobs.put(Version.REBUILD_MESSAGE_FTS_INDEX_3, new RebuildMessageSearchIndexMigrationJob()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/DecryptionsDrainedMigrationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/migrations/DecryptionsDrainedMigrationJob.kt deleted file mode 100644 index d26b665342..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/DecryptionsDrainedMigrationJob.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.thoughtcrime.securesms.migrations - -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobs.PushDecryptDrainedJob - -/** - * Kicks off a job to notify the [org.thoughtcrime.securesms.messages.IncomingMessageObserver] when the decryption queue is empty. - */ -internal class DecryptionsDrainedMigrationJob( - parameters: Parameters = Parameters.Builder().build() -) : MigrationJob(parameters) { - - companion object { - val TAG = Log.tag(DecryptionsDrainedMigrationJob::class.java) - const val KEY = "DecryptionsDrainedMigrationJob" - } - - override fun getFactoryKey(): String = KEY - - override fun isUiBlocking(): Boolean = false - - override fun performMigration() { - ApplicationDependencies.getJobManager().add(PushDecryptDrainedJob()) - } - - override fun shouldRetry(e: Exception): Boolean = false - - class Factory : Job.Factory { - override fun create(parameters: Parameters, serializedData: ByteArray?): DecryptionsDrainedMigrationJob { - return DecryptionsDrainedMigrationJob(parameters) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java index 2ce4b47a88..fbdb1c3263 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java @@ -12,16 +12,13 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.AttachmentTable; import org.thoughtcrime.securesms.database.MessageTable; import org.thoughtcrime.securesms.database.MessageTable.MmsReader; -import org.thoughtcrime.securesms.database.PushTable; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; import org.thoughtcrime.securesms.jobs.PreKeysSyncJob; -import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mms.GlideApp; @@ -30,7 +27,6 @@ import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.FileUtils; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.VersionTracker; -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import java.io.File; import java.util.List; @@ -131,20 +127,20 @@ public class LegacyMigrationJob extends MigrationJob { PreKeysSyncJob.enqueueIfNeeded(); } - if (lastSeenVersion < NO_DECRYPT_QUEUE_VERSION) { - scheduleMessagesInPushDatabase(context); - } +// if (lastSeenVersion < NO_DECRYPT_QUEUE_VERSION) { +// scheduleMessagesInPushDatabase(context); +// } - if (lastSeenVersion < PUSH_DECRYPT_SERIAL_ID_VERSION) { - scheduleMessagesInPushDatabase(context); - } +// if (lastSeenVersion < PUSH_DECRYPT_SERIAL_ID_VERSION) { +// scheduleMessagesInPushDatabase(context); +// } - if (lastSeenVersion < MIGRATE_SESSION_PLAINTEXT) { -// new TextSecureSessionStore(context, masterSecret).migrateSessions(); -// new TextSecurePreKeyStore(context, masterSecret).migrateRecords(); - - scheduleMessagesInPushDatabase(context);; - } +// if (lastSeenVersion < MIGRATE_SESSION_PLAINTEXT) { +//// new TextSecureSessionStore(context, masterSecret).migrateSessions(); +//// new TextSecurePreKeyStore(context, masterSecret).migrateRecords(); +// +// scheduleMessagesInPushDatabase(context);; +// } if (lastSeenVersion < CONTACTS_ACCOUNT_VERSION) { ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false)); @@ -184,9 +180,9 @@ public class LegacyMigrationJob extends MigrationJob { } } - if (lastSeenVersion < SQLCIPHER) { - scheduleMessagesInPushDatabase(context); - } +// if (lastSeenVersion < SQLCIPHER) { +// scheduleMessagesInPushDatabase(context); +// } if (lastSeenVersion < SQLCIPHER_COMPLETE) { File file = context.getDatabasePath("messages.db"); @@ -264,17 +260,17 @@ public class LegacyMigrationJob extends MigrationJob { } } - private static void scheduleMessagesInPushDatabase(@NonNull Context context) { - PushTable pushDatabase = SignalDatabase.push(); - JobManager jobManager = ApplicationDependencies.getJobManager(); - - try (PushTable.Reader pushReader = pushDatabase.readerFor(pushDatabase.getPending())) { - SignalServiceEnvelope envelope; - while ((envelope = pushReader.getNext()) != null) { - jobManager.add(new PushDecryptMessageJob(envelope)); - } - } - } +// private static void scheduleMessagesInPushDatabase(@NonNull Context context) { +// PushTable pushDatabase = SignalDatabase.push(); +// JobManager jobManager = ApplicationDependencies.getJobManager(); +// +// try (PushTable.Reader pushReader = pushDatabase.readerFor(pushDatabase.getPending())) { +// SignalServiceEnvelope envelope; +// while ((envelope = pushReader.getNext()) != null) { +// jobManager.add(new PushDecryptMessageJob(envelope)); +// } +// } +// } public interface DatabaseUpgradeListener { void setProgress(int progress, int total); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/EarlyMessageCache.java b/app/src/main/java/org/thoughtcrime/securesms/util/EarlyMessageCache.java index 245e5f16c6..7b4986e319 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/EarlyMessageCache.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/EarlyMessageCache.java @@ -4,14 +4,12 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.model.ServiceMessageId; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.whispersystems.signalservice.api.messages.SignalServiceContent; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Optional; -import java.util.Set; /** * Sometimes a message that is referencing another message can arrive out of order. In these cases, @@ -20,32 +18,18 @@ import java.util.Set; */ public final class EarlyMessageCache { - private final LRUCache> cache = new LRUCache<>(100); - private final LRUCache> cacheV2 = new LRUCache<>(100); + private final LRUCache> cache = new LRUCache<>(100); /** * @param targetSender The sender of the message this message depends on. * @param targetSentTimestamp The sent timestamp of the message this message depends on. */ - public synchronized void store(@NonNull RecipientId targetSender, long targetSentTimestamp, @NonNull SignalServiceContent content) { - ServiceMessageId messageId = new ServiceMessageId(targetSender, targetSentTimestamp); - List contentList = cache.get(messageId); - - if (contentList == null) { - contentList = new LinkedList<>(); - } - - contentList.add(content); - - cache.put(messageId, contentList); - } - public synchronized void store(@NonNull RecipientId targetSender, long targetSentTimestamp, @NonNull EarlyMessageCacheEntry cacheEntry) { ServiceMessageId messageId = new ServiceMessageId(targetSender, targetSentTimestamp); - List envelopeList = cacheV2.get(messageId); + List envelopeList = cache.get(messageId); if (envelopeList == null) { envelopeList = new LinkedList<>(); @@ -53,7 +37,7 @@ public final class EarlyMessageCache { envelopeList.add(cacheEntry); - cacheV2.put(messageId, envelopeList); + cache.put(messageId, envelopeList); } /** @@ -62,21 +46,15 @@ public final class EarlyMessageCache { * @param sender The sender of the message in question. * @param sentTimestamp The sent timestamp of the message in question. */ - public synchronized Optional> retrieve(@NonNull RecipientId sender, long sentTimestamp) { + public synchronized Optional> retrieve(@NonNull RecipientId sender, long sentTimestamp) { return Optional.ofNullable(cache.remove(new ServiceMessageId(sender, sentTimestamp))); } - public synchronized Optional> retrieveV2(@NonNull RecipientId sender, long sentTimestamp) { - return Optional.ofNullable(cacheV2.remove(new ServiceMessageId(sender, sentTimestamp))); - } - /** * Returns a collection of all of the {@link ServiceMessageId}s referenced in the cache at the moment of inquiry. * Caution: There is no guarantee that this list will be relevant for any amount of time afterwards. */ public synchronized @NonNull Collection getAllReferencedIds() { - Set allIds = new HashSet<>(cache.keySet()); - allIds.addAll(cacheV2.keySet()); - return allIds; + return new HashSet<>(cache.keySet()); } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index c8450ee2ce..0a5b68562b 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -15,11 +15,9 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; -import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; @@ -29,8 +27,6 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf import org.whispersystems.signalservice.internal.push.IdentityCheckRequest; import org.whispersystems.signalservice.internal.push.IdentityCheckResponse; import org.whispersystems.signalservice.internal.push.PushServiceSocket; -import org.whispersystems.signalservice.internal.push.SignalServiceEnvelopeEntity; -import org.whispersystems.signalservice.internal.push.SignalServiceMessagesResult; import org.whispersystems.signalservice.internal.sticker.StickerProtos; import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.util.concurrent.FutureTransformers; @@ -43,7 +39,6 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Optional; @@ -201,67 +196,4 @@ public class SignalServiceMessageReceiver { return new SignalServiceStickerManifest(pack.getTitle(), pack.getAuthor(), cover, stickers); } - - public List retrieveMessages(boolean allowStories, MessageReceivedCallback callback) - throws IOException - { - List results = new LinkedList<>(); - SignalServiceMessagesResult messageResult = socket.getMessages(allowStories); - - for (SignalServiceEnvelopeEntity entity : messageResult.getEnvelopes()) { - SignalServiceEnvelope envelope; - - if (entity.hasSource() && entity.getSourceDevice() > 0) { - SignalServiceAddress address = new SignalServiceAddress(ServiceId.parseOrThrow(entity.getSourceUuid()), entity.getSourceE164()); - envelope = new SignalServiceEnvelope(entity.getType(), - Optional.of(address), - entity.getSourceDevice(), - entity.getTimestamp(), - entity.getContent(), - entity.getServerTimestamp(), - messageResult.getServerDeliveredTimestamp(), - entity.getServerUuid(), - entity.getDestinationUuid(), - entity.isUrgent(), - entity.isStory(), - entity.getReportSpamToken()); - } else { - envelope = new SignalServiceEnvelope(entity.getType(), - entity.getTimestamp(), - entity.getContent(), - entity.getServerTimestamp(), - messageResult.getServerDeliveredTimestamp(), - entity.getServerUuid(), - entity.getDestinationUuid(), - entity.isUrgent(), - entity.isStory(), - entity.getReportSpamToken()); - } - - callback.onMessage(envelope); - results.add(envelope); - - if (envelope.hasServerGuid()) { - socket.acknowledgeMessage(envelope.getServerGuid()); - } else { - socket.acknowledgeMessage(entity.getSourceE164(), entity.getTimestamp()); - } - } - - return results; - } - - public void setSoTimeoutMillis(long soTimeoutMillis) { - socket.setSoTimeoutMillis(soTimeoutMillis); - } - - public interface MessageReceivedCallback { - public void onMessage(SignalServiceEnvelope envelope); - } - - public static class NullMessageReceivedCallback implements MessageReceivedCallback { - @Override - public void onMessage(SignalServiceEnvelope envelope) {} - } - } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java index 3fe1b10b48..5080028c52 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java @@ -44,28 +44,22 @@ import org.signal.libsignal.protocol.message.SignalMessage; import org.whispersystems.signalservice.api.InvalidMessageStructureException; import org.whispersystems.signalservice.api.SignalServiceAccountDataStore; import org.whispersystems.signalservice.api.SignalSessionLock; -import org.whispersystems.signalservice.api.messages.SignalServiceContent; -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceMetadata; -import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.ServiceId; +import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; import org.whispersystems.signalservice.internal.push.PushTransportDetails; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope; -import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; -import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer; -import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer; -import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto; import java.util.Collections; import java.util.List; import java.util.Optional; /** - * This is used to encrypt + decrypt received {@link SignalServiceEnvelope}s. + * This is used to encrypt + decrypt received envelopes. */ public class SignalServiceCipher { @@ -129,41 +123,6 @@ public class SignalServiceCipher { } } - /** - * Decrypt a received {@link SignalServiceEnvelope} - * - * @param envelope The received SignalServiceEnvelope - * - * @return a decrypted SignalServiceContent - */ - public SignalServiceContent decrypt(SignalServiceEnvelope envelope) - throws InvalidMetadataMessageException, InvalidMetadataVersionException, - ProtocolInvalidKeyIdException, ProtocolLegacyMessageException, - ProtocolUntrustedIdentityException, ProtocolNoSessionException, - ProtocolInvalidVersionException, ProtocolInvalidMessageException, - ProtocolInvalidKeyException, ProtocolDuplicateMessageException, - SelfSendException, UnsupportedDataMessageException, InvalidMessageStructureException - { - try { - if (envelope.hasContent()) { - Plaintext plaintext = decryptInternal(envelope.getProto(), envelope.getServerDeliveredTimestamp()); - SignalServiceProtos.Content content = SignalServiceProtos.Content.parseFrom(plaintext.getData()); - - SignalServiceContentProto contentProto = SignalServiceContentProto.newBuilder() - .setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress)) - .setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(plaintext.metadata)) - .setContent(content) - .build(); - - return SignalServiceContent.createFromProto(contentProto); - } - - return null; - } catch (InvalidProtocolBufferException e) { - throw new InvalidMetadataMessageException(e); - } - } - public SignalServiceCipherResult decrypt(Envelope envelope, long serverDeliveredTimestamp) throws InvalidMetadataMessageException, InvalidMetadataVersionException, ProtocolInvalidKeyIdException, ProtocolLegacyMessageException, diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java deleted file mode 100644 index e011a7cda2..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -package org.whispersystems.signalservice.api.messages; - -import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; - -import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.Preconditions; -import org.whispersystems.signalservice.api.util.UuidUtil; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope; -import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceEnvelopeProto; -import org.whispersystems.util.Base64; - -import java.io.IOException; -import java.util.Optional; - -/** - * This class represents an encrypted Signal Service envelope. - * - * The envelope contains the wrapping information, such as the sender, the - * message timestamp, the encrypted message type, etc. - * - * @author Moxie Marlinspike - */ -public class SignalServiceEnvelope { - - private static final String TAG = SignalServiceEnvelope.class.getSimpleName(); - - private final Envelope envelope; - private final long serverDeliveredTimestamp; - - /** - * Construct an envelope from a serialized, Base64 encoded SignalServiceEnvelope, encrypted - * with a signaling key. - * - * @param message The serialized SignalServiceEnvelope, base64 encoded and encrypted. - */ - public SignalServiceEnvelope(String message, long serverDeliveredTimestamp) throws IOException { - this(Base64.decode(message), serverDeliveredTimestamp); - } - - /** - * Construct an envelope from a serialized SignalServiceEnvelope, encrypted with a signaling key. - * - * @param input The serialized and (optionally) encrypted SignalServiceEnvelope. - */ - public SignalServiceEnvelope(byte[] input, long serverDeliveredTimestamp) throws IOException { - this.envelope = Envelope.parseFrom(input); - this.serverDeliveredTimestamp = serverDeliveredTimestamp; - } - - public SignalServiceEnvelope(int type, - Optional sender, - int senderDevice, - long timestamp, - byte[] content, - long serverReceivedTimestamp, - long serverDeliveredTimestamp, - String uuid, - String destinationServiceId, - boolean urgent, - boolean story, - byte[] reportingToken) - { - Envelope.Builder builder = Envelope.newBuilder() - .setType(Envelope.Type.valueOf(type)) - .setSourceDevice(senderDevice) - .setTimestamp(timestamp) - .setServerTimestamp(serverReceivedTimestamp) - .setDestinationServiceId(destinationServiceId) - .setUrgent(urgent) - .setStory(story); - - if (sender.isPresent()) { - builder.setSourceServiceId(sender.get().getServiceId().toString()); - } - - if (uuid != null) { - builder.setServerGuid(uuid); - } - - if (content != null) { - builder.setContent(ByteString.copyFrom(content)); - } - - if (reportingToken != null) { - builder.setReportingToken(ByteString.copyFrom(reportingToken)); - } - - this.envelope = builder.build(); - this.serverDeliveredTimestamp = serverDeliveredTimestamp; - } - - public SignalServiceEnvelope(int type, - long timestamp, - byte[] content, - long serverReceivedTimestamp, - long serverDeliveredTimestamp, - String uuid, - String destinationServiceId, - boolean urgent, - boolean story, - byte[] reportingToken) - { - Envelope.Builder builder = Envelope.newBuilder() - .setType(Envelope.Type.valueOf(type)) - .setTimestamp(timestamp) - .setServerTimestamp(serverReceivedTimestamp) - .setDestinationServiceId(destinationServiceId) - .setUrgent(urgent) - .setStory(story); - - if (uuid != null) { - builder.setServerGuid(uuid); - } - - if (content != null) { - builder.setContent(ByteString.copyFrom(content)); - } - - if (reportingToken != null) { - builder.setReportingToken(ByteString.copyFrom(reportingToken)); - } - - this.envelope = builder.build(); - this.serverDeliveredTimestamp = serverDeliveredTimestamp; - } - - public String getServerGuid() { - return envelope.getServerGuid(); - } - - public boolean hasServerGuid() { - return envelope.hasServerGuid(); - } - - /** - * @return True if either a source E164 or UUID is present. - */ - public boolean hasSourceServiceId() { - return envelope.hasSourceServiceId(); - } - - /** - * @return The envelope's sender as a UUID. - */ - public Optional getSourceServiceId() { - return Optional.ofNullable(envelope.getSourceServiceId()); - } - - public String getSourceIdentifier() { - return getSourceServiceId().get().toString(); - } - - public boolean hasSourceDevice() { - return envelope.hasSourceDevice(); - } - - /** - * @return The envelope's sender device ID. - */ - public int getSourceDevice() { - return envelope.getSourceDevice(); - } - - /** - * @return The envelope content type. - */ - public int getType() { - return envelope.getType().getNumber(); - } - - /** - * @return The timestamp this envelope was sent. - */ - public long getTimestamp() { - return envelope.getTimestamp(); - } - - /** - * @return The server timestamp of when the server received the envelope. - */ - public long getServerReceivedTimestamp() { - return envelope.getServerTimestamp(); - } - - /** - * @return The server timestamp of when the envelope was delivered to us. - */ - public long getServerDeliveredTimestamp() { - return serverDeliveredTimestamp; - } - - /** - * @return Whether the envelope contains an encrypted SignalServiceContent - */ - public boolean hasContent() { - return envelope.hasContent(); - } - - /** - * @return The envelope's encrypted SignalServiceContent. - */ - public byte[] getContent() { - return envelope.getContent().toByteArray(); - } - - /** - * @return true if the containing message is a {@link org.signal.libsignal.protocol.message.SignalMessage} - */ - public boolean isSignalMessage() { - return envelope.getType().getNumber() == Envelope.Type.CIPHERTEXT_VALUE; - } - - /** - * @return true if the containing message is a {@link org.signal.libsignal.protocol.message.PreKeySignalMessage} - */ - public boolean isPreKeySignalMessage() { - return envelope.getType().getNumber() == Envelope.Type.PREKEY_BUNDLE_VALUE; - } - - /** - * @return true if the containing message is a delivery receipt. - */ - public boolean isReceipt() { - return envelope.getType().getNumber() == Envelope.Type.RECEIPT_VALUE; - } - - public boolean isUnidentifiedSender() { - return envelope.getType().getNumber() == Envelope.Type.UNIDENTIFIED_SENDER_VALUE; - } - - public boolean isPlaintextContent() { - return envelope.getType().getNumber() == Envelope.Type.PLAINTEXT_CONTENT_VALUE; - } - - public boolean hasDestinationUuid() { - return envelope.hasDestinationServiceId() && UuidUtil.isUuid(envelope.getDestinationServiceId()); - } - - public String getDestinationServiceId() { - return envelope.getDestinationServiceId(); - } - - public boolean isUrgent() { - return envelope.getUrgent(); - } - - public boolean isStory() { - return envelope.getStory(); - } - - public boolean hasReportingToken() { - return envelope.hasReportingToken(); - } - - public byte[] getReportingToken() { - return envelope.getReportingToken().toByteArray(); - } - - public Envelope getProto() { - return envelope; - } - - private SignalServiceEnvelopeProto.Builder serializeToProto() { - SignalServiceEnvelopeProto.Builder builder = SignalServiceEnvelopeProto.newBuilder() - .setType(getType()) - .setDeviceId(getSourceDevice()) - .setTimestamp(getTimestamp()) - .setServerReceivedTimestamp(getServerReceivedTimestamp()) - .setServerDeliveredTimestamp(getServerDeliveredTimestamp()) - .setUrgent(isUrgent()) - .setStory(isStory()); - - if (getSourceServiceId().isPresent()) { - builder.setSourceServiceId(getSourceServiceId().get()); - } - - if (hasContent()) { - builder.setContent(ByteString.copyFrom(getContent())); - } - - if (hasServerGuid()) { - builder.setServerGuid(getServerGuid()); - } - - if (hasDestinationUuid()) { - builder.setDestinationServiceId(getDestinationServiceId()); - } - - if (hasReportingToken()) { - builder.setReportingToken(ByteString.copyFrom(getReportingToken())); - } - - return builder; - } - - public byte[] serialize() { - return serializeToProto().build().toByteArray(); - } - - public static SignalServiceEnvelope deserialize(byte[] serialized) { - SignalServiceEnvelopeProto proto = null; - try { - proto = SignalServiceEnvelopeProto.parseFrom(serialized); - } catch (InvalidProtocolBufferException e) { - e.printStackTrace(); - } - - Preconditions.checkNotNull(proto); - - ServiceId sourceServiceId = proto.hasSourceServiceId() ? ServiceId.parseOrNull(proto.getSourceServiceId()) : null; - - return new SignalServiceEnvelope(proto.getType(), - sourceServiceId != null ? Optional.of(new SignalServiceAddress(sourceServiceId)) : Optional.empty(), - proto.getDeviceId(), - proto.getTimestamp(), - proto.hasContent() ? proto.getContent().toByteArray() : null, - proto.getServerReceivedTimestamp(), - proto.getServerDeliveredTimestamp(), - proto.getServerGuid(), - proto.getDestinationServiceId(), - proto.getUrgent(), - proto.getStory(), - proto.hasReportingToken() ? proto.getReportingToken().toByteArray() : null); - } -}