diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/EditMessageSyncProcessorTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/EditMessageSyncProcessorTest.kt new file mode 100644 index 0000000000..19c1ffffb3 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/EditMessageSyncProcessorTest.kt @@ -0,0 +1,250 @@ +package org.thoughtcrime.securesms.messages + +import android.database.Cursor +import android.util.Base64 +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.signal.core.util.ThreadUtil +import org.signal.core.util.readToList +import org.signal.core.util.select +import org.signal.core.util.withinTransaction +import org.thoughtcrime.securesms.database.AttachmentTable +import org.thoughtcrime.securesms.database.MessageTable +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.ThreadTable +import org.thoughtcrime.securesms.database.model.toBodyRangeList +import org.thoughtcrime.securesms.mms.OutgoingMessage +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.testing.MessageContentFuzzer +import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.testing.assertIs +import org.thoughtcrime.securesms.util.MessageTableUtils +import org.whispersystems.signalservice.internal.push.SignalServiceProtos +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage +import kotlin.time.Duration.Companion.seconds + +@RunWith(AndroidJUnit4::class) +class EditMessageSyncProcessorTest { + + companion object { + private val IGNORE_MESSAGE_COLUMNS = listOf( + MessageTable.DATE_RECEIVED, + MessageTable.NOTIFIED_TIMESTAMP, + MessageTable.REACTIONS_LAST_SEEN, + MessageTable.NOTIFIED + ) + + private val IGNORE_ATTACHMENT_COLUMNS = listOf( + AttachmentTable.UNIQUE_ID, + AttachmentTable.TRANSFER_FILE + ) + } + + @get:Rule + val harness = SignalActivityRule() + + private lateinit var processorV2: MessageContentProcessorV2 + private lateinit var testResult: TestResults + private var envelopeTimestamp: Long = 0 + + @Before + fun setup() { + processorV2 = MessageContentProcessorV2(harness.context) + envelopeTimestamp = System.currentTimeMillis() + testResult = TestResults() + } + + @Test + fun textMessage() { + var originalTimestamp = envelopeTimestamp + 200 + for (i in 1..10) { + originalTimestamp += 400 + + val toRecipient = Recipient.resolved(harness.others[0]) + + val content = MessageContentFuzzer.fuzzTextMessage() + val metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, toRecipient.id) + val syncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage( + SignalServiceProtos.SyncMessage.newBuilder().setSent( + SignalServiceProtos.SyncMessage.Sent.newBuilder() + .setDestinationUuid(metadata.destinationServiceId.toString()) + .setTimestamp(originalTimestamp) + .setExpirationStartTimestamp(originalTimestamp) + .setMessage(content.dataMessage) + ) + ).build() + SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer) + val syncTextMessage = TestMessage( + envelope = MessageContentFuzzer.envelope(originalTimestamp), + content = syncContent, + metadata = metadata, + serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(originalTimestamp) + ) + + val editTimestamp = originalTimestamp + 200 + val editedContent = MessageContentFuzzer.fuzzTextMessage() + val editSyncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage( + SignalServiceProtos.SyncMessage.newBuilder().setSent( + SignalServiceProtos.SyncMessage.Sent.newBuilder() + .setDestinationUuid(metadata.destinationServiceId.toString()) + .setTimestamp(editTimestamp) + .setExpirationStartTimestamp(editTimestamp) + .setEditMessage( + EditMessage.newBuilder() + .setDataMessage(editedContent.dataMessage) + .setTargetSentTimestamp(originalTimestamp) + ) + ) + ).build() + + val syncEditMessage = TestMessage( + envelope = MessageContentFuzzer.envelope(editTimestamp), + content = editSyncContent, + metadata = metadata, + serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(editTimestamp) + ) + + testResult.runSync(listOf(syncTextMessage, syncEditMessage)) + + SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer / 1000) + val originalTextMessage = OutgoingMessage( + threadRecipient = toRecipient, + sentTimeMillis = originalTimestamp, + body = content.dataMessage.body, + expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds, + isUrgent = true, + isSecure = true, + bodyRanges = content.dataMessage.bodyRangesList.toBodyRangeList() + ) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient) + val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null) + SignalDatabase.messages.markAsSent(originalMessageId, true) + if (content.dataMessage.expireTimer > 0) { + SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp) + } + + val editMessage = OutgoingMessage( + threadRecipient = toRecipient, + sentTimeMillis = editTimestamp, + body = editedContent.dataMessage.body, + expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds, + isUrgent = true, + isSecure = true, + bodyRanges = editedContent.dataMessage.bodyRangesList.toBodyRangeList(), + messageToEdit = originalMessageId + ) + + val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null) + SignalDatabase.messages.markAsSent(editMessageId, true) + + if (content.dataMessage.expireTimer > 0) { + SignalDatabase.messages.markExpireStarted(editMessageId, originalTimestamp) + } + testResult.collectLocal() + testResult.assert() + } + } + + private inner class TestResults { + + private lateinit var localMessages: List>> + private lateinit var localAttachments: List>> + + private lateinit var syncMessages: List>> + private lateinit var syncAttachments: List>> + + fun collectLocal() { + harness.inMemoryLogger.clear() + + localMessages = dumpMessages() + localAttachments = dumpAttachments() + + cleanup() + } + + fun runSync(messages: List) { + messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) -> + if (content.hasSyncMessage()) { + processorV2.process( + envelope, + content, + metadata, + serverDeliveredTimestamp, + false + ) + ThreadUtil.sleep(1) + } + } + harness.inMemoryLogger.clear() + + syncMessages = dumpMessages() + syncAttachments = dumpAttachments() + + cleanup() + } + + fun cleanup() { + SignalDatabase.rawDatabase.withinTransaction { db -> + SignalDatabase.threads.deleteAllConversations() + db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${MessageTable.TABLE_NAME}'") + db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${ThreadTable.TABLE_NAME}'") + db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${AttachmentTable.TABLE_NAME}'") + } + } + + fun assert() { + syncMessages.zip(localMessages) + .forEach { (v2, v1) -> + v2.assertIs(v1) + } + + syncAttachments.zip(localAttachments) + .forEach { (v2, v1) -> + v2.assertIs(v1) + } + } + + private fun dumpMessages(): List>> { + return dumpTable(MessageTable.TABLE_NAME) + .map { row -> + val newRow = row.toMutableList() + newRow.removeIf { IGNORE_MESSAGE_COLUMNS.contains(it.first) } + newRow + } + } + + private fun dumpAttachments(): List>> { + return dumpTable(AttachmentTable.TABLE_NAME) + .map { row -> + val newRow = row.toMutableList() + newRow.removeIf { IGNORE_ATTACHMENT_COLUMNS.contains(it.first) } + newRow + } + } + + private fun dumpTable(table: String): List>> { + return SignalDatabase.rawDatabase + .select() + .from(table) + .run() + .readToList { cursor -> + val map: List> = cursor.columnNames.map { column -> + val index = cursor.getColumnIndex(column) + var data: String? = when (cursor.getType(index)) { + Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0) + else -> cursor.getString(index) + } + if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) { + data = MessageTableUtils.typeColumnToString(cursor.getLong(index)) + } + + column to data + } + map + } + } + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt index 6a4f014b1e..ceb13397b2 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt @@ -13,8 +13,6 @@ import org.signal.core.util.select import org.signal.core.util.withinTransaction import org.thoughtcrime.securesms.database.AttachmentTable import org.thoughtcrime.securesms.database.MessageTable -import org.thoughtcrime.securesms.database.MessageTypes -import org.thoughtcrime.securesms.database.MessageTypes.isOutgoingMessageType import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.ThreadTable import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -23,6 +21,7 @@ import org.thoughtcrime.securesms.testing.InMemoryLogger import org.thoughtcrime.securesms.testing.MessageContentFuzzer import org.thoughtcrime.securesms.testing.SignalActivityRule import org.thoughtcrime.securesms.testing.assertIs +import org.thoughtcrime.securesms.util.MessageTableUtils import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata import org.whispersystems.signalservice.api.messages.SignalServiceContent import org.whispersystems.signalservice.api.messages.SignalServiceMetadata @@ -279,7 +278,7 @@ class MessageContentProcessorTestV2 { else -> cursor.getString(index) } if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) { - data = typeColumnToString(cursor.getLong(index)) + data = MessageTableUtils.typeColumnToString(cursor.getLong(index)) } column to data @@ -311,64 +310,4 @@ class MessageContentProcessorTestV2 { return SignalServiceContent.createFromProto(contentProto)!! } - - fun typeColumnToString(type: Long): String { - return """ - isOutgoingMessageType:${isOutgoingMessageType(type)} - isForcedSms:${type and MessageTypes.MESSAGE_FORCE_SMS_BIT != 0L} - isDraftMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_DRAFT_TYPE} - isFailedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_FAILED_TYPE} - isPendingMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_OUTBOX_TYPE || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENDING_TYPE} - isSentType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_TYPE} - isPendingSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK} - isPendingSecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK} - isPendingInsecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK} - isInboxType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_INBOX_TYPE} - isJoinedType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.JOINED_TYPE} - isUnsupportedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.UNSUPPORTED_MESSAGE_TYPE} - isInvalidMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.INVALID_MESSAGE_TYPE} - isBadDecryptType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BAD_DECRYPT_TYPE} - isSecureType:${type and MessageTypes.SECURE_MESSAGE_BIT != 0L} - isPushType:${type and MessageTypes.PUSH_MESSAGE_BIT != 0L} - isEndSessionType:${type and MessageTypes.END_SESSION_BIT != 0L} - isKeyExchangeType:${type and MessageTypes.KEY_EXCHANGE_BIT != 0L} - isIdentityVerified:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L} - isIdentityDefault:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L} - isCorruptedKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CORRUPTED_BIT != 0L} - isInvalidVersionKeyExchange:${type and MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT != 0L} - isBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_BUNDLE_BIT != 0L} - isContentBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT != 0L} - isIdentityUpdate:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L} - isRateLimited:${type and MessageTypes.MESSAGE_RATE_LIMITED_BIT != 0L} - isExpirationTimerUpdate:${type and MessageTypes.EXPIRATION_TIMER_UPDATE_BIT != 0L} - isIncomingAudioCall:${type == MessageTypes.INCOMING_AUDIO_CALL_TYPE} - isIncomingVideoCall:${type == MessageTypes.INCOMING_VIDEO_CALL_TYPE} - isOutgoingAudioCall:${type == MessageTypes.OUTGOING_AUDIO_CALL_TYPE} - isOutgoingVideoCall:${type == MessageTypes.OUTGOING_VIDEO_CALL_TYPE} - isMissedAudioCall:${type == MessageTypes.MISSED_AUDIO_CALL_TYPE} - isMissedVideoCall:${type == MessageTypes.MISSED_VIDEO_CALL_TYPE} - isGroupCall:${type == MessageTypes.GROUP_CALL_TYPE} - isGroupUpdate:${type and MessageTypes.GROUP_UPDATE_BIT != 0L} - isGroupV2:${type and MessageTypes.GROUP_V2_BIT != 0L} - isGroupQuit:${type and MessageTypes.GROUP_LEAVE_BIT != 0L && type and MessageTypes.GROUP_V2_BIT == 0L} - isChatSessionRefresh:${type and MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT != 0L} - isDuplicateMessageType:${type and MessageTypes.ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L} - isDecryptInProgressType:${type and 0x40000000 != 0L} - isNoRemoteSessionType:${type and MessageTypes.ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L} - isLegacyType:${type and MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and MessageTypes.ENCRYPTION_REMOTE_BIT != 0L} - isProfileChange:${type == MessageTypes.PROFILE_CHANGE_TYPE} - isGroupV1MigrationEvent:${type == MessageTypes.GV1_MIGRATION_TYPE} - isChangeNumber:${type == MessageTypes.CHANGE_NUMBER_TYPE} - isBoostRequest:${type == MessageTypes.BOOST_REQUEST_TYPE} - isThreadMerge:${type == MessageTypes.THREAD_MERGE_TYPE} - isSmsExport:${type == MessageTypes.SMS_EXPORT_TYPE} - isGroupV2LeaveOnly:${type and MessageTypes.GROUP_V2_LEAVE_BITS == MessageTypes.GROUP_V2_LEAVE_BITS} - isSpecialType:${type and MessageTypes.SPECIAL_TYPES_MASK != 0L} - isStoryReaction:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_STORY_REACTION} - isGiftBadge:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_GIFT_BADGE} - isPaymentsNotificaiton:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} - isRequestToActivatePayments:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST} - isPaymentsActivated:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED} - """.trimIndent().replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "") - } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/util/MessageTableUtils.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/util/MessageTableUtils.kt new file mode 100644 index 0000000000..c173e1bc6e --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/util/MessageTableUtils.kt @@ -0,0 +1,65 @@ +package org.thoughtcrime.securesms.util + +import org.thoughtcrime.securesms.database.MessageTypes + +object MessageTableUtils { + fun typeColumnToString(type: Long): String { + return """ + isOutgoingMessageType:${MessageTypes.isOutgoingMessageType(type)} + isForcedSms:${type and MessageTypes.MESSAGE_FORCE_SMS_BIT != 0L} + isDraftMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_DRAFT_TYPE} + isFailedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_FAILED_TYPE} + isPendingMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_OUTBOX_TYPE || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENDING_TYPE} + isSentType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_TYPE} + isPendingSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK} + isPendingSecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK} + isPendingInsecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK} + isInboxType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_INBOX_TYPE} + isJoinedType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.JOINED_TYPE} + isUnsupportedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.UNSUPPORTED_MESSAGE_TYPE} + isInvalidMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.INVALID_MESSAGE_TYPE} + isBadDecryptType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BAD_DECRYPT_TYPE} + isSecureType:${type and MessageTypes.SECURE_MESSAGE_BIT != 0L} + isPushType:${type and MessageTypes.PUSH_MESSAGE_BIT != 0L} + isEndSessionType:${type and MessageTypes.END_SESSION_BIT != 0L} + isKeyExchangeType:${type and MessageTypes.KEY_EXCHANGE_BIT != 0L} + isIdentityVerified:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L} + isIdentityDefault:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L} + isCorruptedKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CORRUPTED_BIT != 0L} + isInvalidVersionKeyExchange:${type and MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT != 0L} + isBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_BUNDLE_BIT != 0L} + isContentBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT != 0L} + isIdentityUpdate:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L} + isRateLimited:${type and MessageTypes.MESSAGE_RATE_LIMITED_BIT != 0L} + isExpirationTimerUpdate:${type and MessageTypes.EXPIRATION_TIMER_UPDATE_BIT != 0L} + isIncomingAudioCall:${type == MessageTypes.INCOMING_AUDIO_CALL_TYPE} + isIncomingVideoCall:${type == MessageTypes.INCOMING_VIDEO_CALL_TYPE} + isOutgoingAudioCall:${type == MessageTypes.OUTGOING_AUDIO_CALL_TYPE} + isOutgoingVideoCall:${type == MessageTypes.OUTGOING_VIDEO_CALL_TYPE} + isMissedAudioCall:${type == MessageTypes.MISSED_AUDIO_CALL_TYPE} + isMissedVideoCall:${type == MessageTypes.MISSED_VIDEO_CALL_TYPE} + isGroupCall:${type == MessageTypes.GROUP_CALL_TYPE} + isGroupUpdate:${type and MessageTypes.GROUP_UPDATE_BIT != 0L} + isGroupV2:${type and MessageTypes.GROUP_V2_BIT != 0L} + isGroupQuit:${type and MessageTypes.GROUP_LEAVE_BIT != 0L && type and MessageTypes.GROUP_V2_BIT == 0L} + isChatSessionRefresh:${type and MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT != 0L} + isDuplicateMessageType:${type and MessageTypes.ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L} + isDecryptInProgressType:${type and 0x40000000 != 0L} + isNoRemoteSessionType:${type and MessageTypes.ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L} + isLegacyType:${type and MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and MessageTypes.ENCRYPTION_REMOTE_BIT != 0L} + isProfileChange:${type == MessageTypes.PROFILE_CHANGE_TYPE} + isGroupV1MigrationEvent:${type == MessageTypes.GV1_MIGRATION_TYPE} + isChangeNumber:${type == MessageTypes.CHANGE_NUMBER_TYPE} + isBoostRequest:${type == MessageTypes.BOOST_REQUEST_TYPE} + isThreadMerge:${type == MessageTypes.THREAD_MERGE_TYPE} + isSmsExport:${type == MessageTypes.SMS_EXPORT_TYPE} + isGroupV2LeaveOnly:${type and MessageTypes.GROUP_V2_LEAVE_BITS == MessageTypes.GROUP_V2_LEAVE_BITS} + isSpecialType:${type and MessageTypes.SPECIAL_TYPES_MASK != 0L} + isStoryReaction:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_STORY_REACTION} + isGiftBadge:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_GIFT_BADGE} + isPaymentsNotificaiton:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} + isRequestToActivatePayments:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST} + isPaymentsActivated:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED} + """.trimIndent().replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "") + } +} 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 2c261c5ffa..d10b07d3f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/EditMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/EditMessageProcessor.kt @@ -49,7 +49,7 @@ object EditMessageProcessor { log(envelope.timestamp, "[handleEditMessage] Edit message for " + editMessage.targetSentTimestamp) - var targetMessage: MediaMmsMessageRecord? = SignalDatabase.messages.getMessageFor(editMessage.targetSentTimestamp, senderRecipient.id) as MediaMmsMessageRecord + var targetMessage: MediaMmsMessageRecord? = SignalDatabase.messages.getMessageFor(editMessage.targetSentTimestamp, senderRecipient.id) as? MediaMmsMessageRecord val targetThreadRecipient: Recipient? = if (targetMessage != null) SignalDatabase.threads.getRecipientForThreadId(targetMessage.threadId) else null if (targetMessage == null || targetThreadRecipient == null) { 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 cec359315a..9d57185df9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.IdentityUtil import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.MessageConstraintsUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata @@ -164,6 +165,11 @@ object SyncMessageProcessor { return } + if (sent.hasEditMessage()) { + handleSynchronizeSentEditMessage(context, envelope, sent, senderRecipient, earlyMessageCacheEntry) + return + } + val dataMessage = sent.message val groupId: GroupId.V2? = if (dataMessage.hasGroupContext) GroupId.v2(dataMessage.groupV2.groupMasterKey) else null @@ -229,6 +235,170 @@ object SyncMessageProcessor { } } + @Throws(MmsException::class) + private fun handleSynchronizeSentEditMessage( + context: Context, + envelope: Envelope, + sent: Sent, + senderRecipient: Recipient, + earlyMessageCacheEntry: EarlyMessageCacheEntry? + ) { + val targetSentTimestamp: Long = sent.editMessage.targetSentTimestamp + val targetMessage: MessageRecord? = SignalDatabase.messages.getMessageFor(targetSentTimestamp, senderRecipient.id) + val senderRecipientId = senderRecipient.id + + if (targetMessage == null) { + warn(envelope.timestamp, "[handleSynchronizeSentEditMessage] Could not find matching message! targetTimestamp: $targetSentTimestamp author: $senderRecipientId") + if (earlyMessageCacheEntry != null) { + ApplicationDependencies.getEarlyMessageCache().store(senderRecipientId, targetSentTimestamp, earlyMessageCacheEntry) + PushProcessEarlyMessagesJob.enqueue() + } + } else if (MessageConstraintsUtil.isValidEditMessageReceive(targetMessage, senderRecipient, envelope.serverTimestamp)) { + val message = sent.editMessage.dataMessage + val toRecipient: Recipient = if (message.hasGroupContext) { + Recipient.externalPossiblyMigratedGroup(GroupId.v2(message.groupV2.groupMasterKey)) + } else { + Recipient.externalPush(ServiceId.parseOrThrow(sent.destinationUuid)) + } + if (message.isMediaMessage) { + handleSynchronizeSentEditMediaMessage(context, targetMessage, toRecipient, sent, message, envelope.timestamp) + } else { + handleSynchronizeSentEditTextMessage(targetMessage, toRecipient, sent, message, envelope.timestamp) + } + } else { + warn(envelope.timestamp, "[handleSynchronizeSentEditMessage] Invalid message edit! editTime: ${envelope.serverTimestamp}, targetTime: ${targetMessage.serverTimestamp}, sendAuthor: $senderRecipientId, targetAuthor: ${targetMessage.fromRecipient.id}") + } + } + + private fun handleSynchronizeSentEditTextMessage( + targetMessage: MessageRecord, + toRecipient: Recipient, + sent: Sent, + message: DataMessage, + envelopeTimestamp: Long + ) { + log(envelopeTimestamp, "Synchronize sent edit text message for message: ${targetMessage.id}") + + val body = message.body ?: "" + val bodyRanges = message.bodyRangesList.filterNot { it.hasMentionUuid() }.toBodyRangeList() + + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient) + val isGroup = toRecipient.isGroup + val messageId: Long + + if (isGroup) { + val outgoingMessage = OutgoingMessage( + recipient = toRecipient, + body = body, + timestamp = sent.timestamp, + expiresIn = targetMessage.expiresIn, + isSecure = true, + bodyRanges = bodyRanges, + messageToEdit = targetMessage.id + ) + + messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null) + updateGroupReceiptStatus(sent, messageId, toRecipient.requireGroupId()) + } else { + val outgoingTextMessage = OutgoingMessage( + threadRecipient = toRecipient, + sentTimeMillis = sent.timestamp, + body = body, + expiresIn = targetMessage.expiresIn, + isUrgent = true, + isSecure = true, + bodyRanges = bodyRanges, + messageToEdit = targetMessage.id + ) + messageId = SignalDatabase.messages.insertMessageOutbox(outgoingTextMessage, threadId, false, null) + SignalDatabase.messages.markUnidentified(messageId, sent.isUnidentified(toRecipient.serviceId.orNull())) + } + SignalDatabase.threads.update(threadId, true) + SignalDatabase.messages.markAsSent(messageId, true) + if (targetMessage.expireStarted > 0) { + SignalDatabase.messages.markExpireStarted(messageId, targetMessage.expireStarted) + ApplicationDependencies.getExpiringMessageManager().scheduleDeletion(messageId, true, targetMessage.expireStarted, targetMessage.expireStarted) + } + + if (toRecipient.isSelf) { + SignalDatabase.messages.incrementDeliveryReceiptCount(sent.timestamp, toRecipient.id, System.currentTimeMillis()) + SignalDatabase.messages.incrementReadReceiptCount(sent.timestamp, toRecipient.id, System.currentTimeMillis()) + } + } + + private fun handleSynchronizeSentEditMediaMessage( + context: Context, + targetMessage: MessageRecord, + toRecipient: Recipient, + sent: Sent, + message: DataMessage, + envelopeTimestamp: Long + ) { + log(envelopeTimestamp, "Synchronize sent edit media message for: ${targetMessage.id}") + + val quote: QuoteModel? = DataMessageProcessor.getValidatedQuote(context, envelopeTimestamp, message) + val sharedContacts: List = DataMessageProcessor.getContacts(message) + val previews: List = DataMessageProcessor.getLinkPreviews(message.previewList, message.body ?: "", false) + val mentions: List = DataMessageProcessor.getMentions(message.bodyRangesList) + val viewOnce: Boolean = message.isViewOnce + val bodyRanges: BodyRangeList? = message.bodyRangesList.toBodyRangeList() + + val syncAttachments = message.attachmentsList.toPointers().filter { + MediaUtil.SlideType.LONG_TEXT == MediaUtil.getSlideTypeFromContentType(it.contentType) + } + + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient) + val messageId: Long + val attachments: List + val mediaMessage = OutgoingMessage( + recipient = toRecipient, + body = message.body ?: "", + attachments = syncAttachments.ifEmpty { (targetMessage as? MediaMmsMessageRecord)?.slideDeck?.asAttachments() ?: emptyList() }, + timestamp = sent.timestamp, + expiresIn = targetMessage.expiresIn, + viewOnce = viewOnce, + quote = quote, + contacts = sharedContacts, + previews = previews, + mentions = mentions, + bodyRanges = bodyRanges, + isSecure = true, + messageToEdit = targetMessage.id + ) + + SignalDatabase.messages.beginTransaction() + try { + messageId = SignalDatabase.messages.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null) + + if (toRecipient.isGroup) { + updateGroupReceiptStatus(sent, messageId, toRecipient.requireGroupId()) + } else { + SignalDatabase.messages.markUnidentified(messageId, sent.isUnidentified(toRecipient.serviceId.orNull())) + } + + SignalDatabase.messages.markAsSent(messageId, true) + + attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId) + + if (targetMessage.expireStarted > 0) { + SignalDatabase.messages.markExpireStarted(messageId, targetMessage.expireStarted) + ApplicationDependencies.getExpiringMessageManager().scheduleDeletion(messageId, true, targetMessage.expireStarted, targetMessage.expireStarted) + } + if (toRecipient.isSelf) { + SignalDatabase.messages.incrementDeliveryReceiptCount(sent.timestamp, toRecipient.id, System.currentTimeMillis()) + SignalDatabase.messages.incrementReadReceiptCount(sent.timestamp, toRecipient.id, System.currentTimeMillis()) + } + SignalDatabase.messages.setTransactionSuccessful() + } finally { + SignalDatabase.messages.endTransaction() + } + if (syncAttachments.isNotEmpty()) { + for (attachment in attachments) { + ApplicationDependencies.getJobManager().add(AttachmentDownloadJob(messageId, attachment.attachmentId, false)) + } + } + } + @Throws(MmsException::class) private fun handleSynchronizeSentStoryMessage(envelope: Envelope, sent: Sent) { log(envelope.timestamp, "Synchronize sent story message for " + sent.timestamp) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MessageConstraintsUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MessageConstraintsUtil.kt index 56f094adf6..9f2d561d38 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MessageConstraintsUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MessageConstraintsUtil.kt @@ -52,7 +52,7 @@ object MessageConstraintsUtil { @JvmStatic fun isValidEditMessageSend(targetMessage: MessageRecord, currentTime: Long): Boolean { return isValidRemoteDeleteSend(targetMessage, currentTime) && - targetMessage.revisionNumber < 10 && + targetMessage.revisionNumber < MAX_EDIT_COUNT && !targetMessage.isViewOnceMessage() && !targetMessage.hasAudio() && !targetMessage.hasSharedContact() diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index a418e4ad78..78bdfcd3b9 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -1332,6 +1332,7 @@ public class SignalServiceMessageSender { SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder(); DataMessage dataMessage = content != null && content.hasDataMessage() ? content.getDataMessage() : null; StoryMessage storyMessage = content != null && content.hasStoryMessage() ? content.getStoryMessage() : null; + EditMessage editMessage = content != null && content.hasEditMessage() ? content.getEditMessage() : null; sentMessage.setTimestamp(timestamp); @@ -1368,6 +1369,10 @@ public class SignalServiceMessageSender { sentMessage.setStoryMessage(storyMessage); } + if (editMessage != null) { + sentMessage.setEditMessage(editMessage); + } + sentMessage.addAllStoryMessageRecipients(storyMessageRecipients.stream() .map(this::createStoryMessageRecipient) .collect(Collectors.toSet())); diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 3c533319dd..fb6a22f9b9 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -471,6 +471,7 @@ message SyncMessage { optional bool isRecipientUpdate = 6 [default = false]; optional StoryMessage storyMessage = 8; repeated StoryMessageRecipient storyMessageRecipients = 9; + optional EditMessage editMessage = 10; } message Contacts {