Add single attachment delete sync.
This commit is contained in:
parent
ea87108def
commit
09003d85b1
13 changed files with 492 additions and 38 deletions
|
@ -5,6 +5,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
|
|||
import org.thoughtcrime.securesms.audio.AudioHash
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import java.util.UUID
|
||||
|
||||
object UriAttachmentBuilder {
|
||||
fun build(
|
||||
|
@ -22,23 +23,28 @@ object UriAttachmentBuilder {
|
|||
stickerLocator: StickerLocator? = null,
|
||||
blurHash: BlurHash? = null,
|
||||
audioHash: AudioHash? = null,
|
||||
transformProperties: AttachmentTable.TransformProperties? = null
|
||||
transformProperties: AttachmentTable.TransformProperties? = null,
|
||||
uuid: UUID? = UUID.randomUUID()
|
||||
): UriAttachment {
|
||||
return UriAttachment(
|
||||
uri,
|
||||
contentType,
|
||||
transferState,
|
||||
size,
|
||||
fileName,
|
||||
voiceNote,
|
||||
borderless,
|
||||
videoGif,
|
||||
quote,
|
||||
caption,
|
||||
stickerLocator,
|
||||
blurHash,
|
||||
audioHash,
|
||||
transformProperties
|
||||
dataUri = uri,
|
||||
contentType = contentType,
|
||||
transferState = transferState,
|
||||
size = size,
|
||||
width = 0,
|
||||
height = 0,
|
||||
fileName = fileName,
|
||||
fastPreflightId = null,
|
||||
voiceNote = voiceNote,
|
||||
borderless = borderless,
|
||||
videoGif = videoGif,
|
||||
quote = quote,
|
||||
caption = caption,
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
audioHash = audioHash,
|
||||
transformProperties = transformProperties,
|
||||
uuid = uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,23 +5,31 @@
|
|||
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.net.Uri
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
||||
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Makes inserting messages through the "normal" code paths simpler. Mostly focused on incoming messages.
|
||||
|
@ -68,7 +76,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
|||
return messageData
|
||||
}
|
||||
|
||||
fun outgoingText(conversationId: RecipientId = alice, successfulSend: Boolean = true, updateMessage: (OutgoingMessage.() -> OutgoingMessage)? = null): MessageData {
|
||||
fun outgoingText(conversationId: RecipientId = alice, successfulSend: Boolean = true, updateMessage: ((OutgoingMessage) -> OutgoingMessage)? = null): MessageData {
|
||||
startTime = nextStartTime()
|
||||
|
||||
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
|
||||
|
@ -80,7 +88,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
|||
sentTimeMillis = messageData.timestamp,
|
||||
isUrgent = true,
|
||||
isSecure = true
|
||||
).apply { updateMessage?.invoke(this) }
|
||||
).let { updateMessage?.invoke(it) ?: it }
|
||||
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
|
||||
|
@ -111,6 +119,20 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
|||
return messageData.copy(messageId = messageId)
|
||||
}
|
||||
|
||||
fun outgoingAttachment(data: ByteArray, uuid: UUID? = UUID.randomUUID()): Attachment {
|
||||
val uri: Uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
|
||||
|
||||
val attachment: UriAttachment = UriAttachmentBuilder.build(
|
||||
id = Random.nextLong(),
|
||||
uri = uri,
|
||||
contentType = MediaUtil.IMAGE_JPEG,
|
||||
transformProperties = AttachmentTable.TransformProperties(),
|
||||
uuid = uuid
|
||||
)
|
||||
|
||||
return attachment
|
||||
}
|
||||
|
||||
fun outgoingGroupChange(): MessageData {
|
||||
startTime = nextStartTime()
|
||||
|
||||
|
@ -238,6 +260,20 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
|||
return messageData
|
||||
}
|
||||
|
||||
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): MessageData {
|
||||
startTime = nextStartTime()
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncDeleteForMeAttachment(conversationId, message, uuid, digest, plainTextHash),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next "sentTimestamp" for current + [nextMessageOffset]th message. Useful for early message processing and future message timestamps.
|
||||
*/
|
||||
|
|
|
@ -13,7 +13,11 @@ import org.junit.Rule
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
|
@ -24,8 +28,11 @@ import org.thoughtcrime.securesms.testing.MessageContentFuzzer.DeleteForMeSync
|
|||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assert
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsSize
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
@ -531,4 +538,154 @@ class SyncMessageProcessorTest_synchronizeDeleteForMe {
|
|||
harness.inMemoryLogger.flush()
|
||||
harness.inMemoryLogger.entries().filter { it.message?.contains("Thread is not local only") == true }.size assertIs 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleAttachmentDeletes() {
|
||||
// GIVEN
|
||||
val message1 = messageHelper.outgoingText { message ->
|
||||
message.copy(
|
||||
attachments = listOf(
|
||||
messageHelper.outgoingAttachment(byteArrayOf(1, 2, 3)),
|
||||
messageHelper.outgoingAttachment(byteArrayOf(2, 3, 4), null),
|
||||
messageHelper.outgoingAttachment(byteArrayOf(5, 6, 7), null),
|
||||
messageHelper.outgoingAttachment(byteArrayOf(10, 11, 12))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
var attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
attachments assertIsSize 4
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
|
||||
// Has all three
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[0].attachmentId,
|
||||
attachment = attachments[0].copy(digest = byteArrayOf(attachments[0].attachmentId.id.toByte())),
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
|
||||
// Missing uuid and digest
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[1].attachmentId,
|
||||
attachment = attachments[1],
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
|
||||
// Missing uuid and plain text
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[2].attachmentId,
|
||||
attachment = attachments[2].copy(digest = byteArrayOf(attachments[2].attachmentId.id.toByte())),
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
SignalDatabase.rawDatabase.update(AttachmentTable.TABLE_NAME).values(AttachmentTable.DATA_HASH_END to null).where("${AttachmentTable.ID} = ?", attachments[2].attachmentId).run()
|
||||
|
||||
// Different has all three
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[3].attachmentId,
|
||||
attachment = attachments[3].copy(digest = byteArrayOf(attachments[3].attachmentId.id.toByte())),
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
|
||||
attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[0].uuid,
|
||||
attachments[0].remoteDigest,
|
||||
attachments[0].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
var updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 3
|
||||
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[0].attachmentId }
|
||||
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[1].uuid,
|
||||
attachments[1].remoteDigest,
|
||||
attachments[1].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 2
|
||||
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[1].attachmentId }
|
||||
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[2].uuid,
|
||||
attachments[2].remoteDigest,
|
||||
attachments[2].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 1
|
||||
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[2].attachmentId }
|
||||
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[3].uuid,
|
||||
attachments[3].remoteDigest,
|
||||
attachments[3].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 0
|
||||
|
||||
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||
}
|
||||
|
||||
private fun DatabaseAttachment.copy(
|
||||
uuid: UUID? = this.uuid,
|
||||
digest: ByteArray? = this.remoteDigest
|
||||
): Attachment {
|
||||
return DatabaseAttachment(
|
||||
attachmentId = this.attachmentId,
|
||||
mmsId = this.mmsId,
|
||||
hasData = this.hasData,
|
||||
hasThumbnail = false,
|
||||
hasArchiveThumbnail = false,
|
||||
contentType = this.contentType,
|
||||
transferProgress = this.transferState,
|
||||
size = this.size,
|
||||
fileName = this.fileName,
|
||||
cdn = this.cdn,
|
||||
location = this.remoteLocation,
|
||||
key = this.remoteKey,
|
||||
digest = digest,
|
||||
incrementalDigest = this.incrementalDigest,
|
||||
incrementalMacChunkSize = this.incrementalMacChunkSize,
|
||||
fastPreflightId = this.fastPreflightId,
|
||||
voiceNote = this.voiceNote,
|
||||
borderless = this.borderless,
|
||||
videoGif = this.videoGif,
|
||||
width = this.width,
|
||||
height = this.height,
|
||||
quote = this.quote,
|
||||
caption = this.caption,
|
||||
stickerLocator = this.stickerLocator,
|
||||
blurHash = this.blurHash,
|
||||
audioHash = this.audioHash,
|
||||
transformProperties = this.transformProperties,
|
||||
displayOrder = this.displayOrder,
|
||||
uploadTimestamp = this.uploadTimestamp,
|
||||
dataHash = this.dataHash,
|
||||
archiveCdn = this.archiveCdn,
|
||||
archiveThumbnailCdn = this.archiveThumbnailCdn,
|
||||
archiveMediaName = this.archiveMediaName,
|
||||
archiveMediaId = this.archiveMediaId,
|
||||
thumbnailRestoreState = this.thumbnailRestoreState,
|
||||
uuid = uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,15 @@ package org.thoughtcrime.securesms.testing
|
|||
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||
import org.thoughtcrime.securesms.messages.TestMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
|
@ -229,6 +232,35 @@ object MessageContentFuzzer {
|
|||
).build()
|
||||
}
|
||||
|
||||
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): Content {
|
||||
val conversation = Recipient.resolved(conversationId)
|
||||
|
||||
return Content
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage(
|
||||
deleteForMe = SyncMessage.DeleteForMe(
|
||||
attachmentDeletes = listOf(
|
||||
SyncMessage.DeleteForMe.AttachmentDelete(
|
||||
conversation = if (conversation.isGroup) {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
|
||||
} else {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
|
||||
},
|
||||
targetMessage = SyncMessage.DeleteForMe.AddressableMessage(
|
||||
authorServiceId = Recipient.resolved(message.first).requireAci().toString(),
|
||||
sentTimestamp = message.second
|
||||
),
|
||||
uuid = uuid?.let { UuidUtil.toByteString(it) },
|
||||
fallbackDigest = digest?.toByteString(),
|
||||
fallbackPlaintextHash = plainTextHash?.let { Base64.decodeOrNull(it)?.toByteString() }
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that may be:
|
||||
* - A text body
|
||||
|
@ -373,7 +405,8 @@ object MessageContentFuzzer {
|
|||
data class DeleteForMeSync(
|
||||
val conversationId: RecipientId,
|
||||
val messages: List<Pair<RecipientId, Long>>,
|
||||
val isFullDelete: Boolean = true
|
||||
val isFullDelete: Boolean = true,
|
||||
val attachments: List<Pair<Long, AttachmentTable.SyncAttachmentId>> = emptyList()
|
||||
) {
|
||||
constructor(conversationId: RecipientId, vararg messages: Pair<RecipientId, Long>) : this(conversationId, messages.toList())
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import android.database.Cursor
|
||||
import android.util.Base64
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.hasSize
|
||||
|
@ -9,6 +8,7 @@ import org.hamcrest.Matchers.`is`
|
|||
import org.hamcrest.Matchers.not
|
||||
import org.hamcrest.Matchers.notNullValue
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.select
|
||||
|
@ -67,27 +67,31 @@ fun CountDownLatch.awaitFor(duration: Duration) {
|
|||
}
|
||||
}
|
||||
|
||||
fun dumpTableToLogs(tag: String = "TestUtils", table: String) {
|
||||
dumpTable(table).forEach { Log.d(tag, it.toString()) }
|
||||
fun dumpTableToLogs(tag: String = "TestUtils", table: String, columns: Set<String>? = null) {
|
||||
dumpTable(table, columns).forEach { Log.d(tag, it.toString()) }
|
||||
}
|
||||
|
||||
fun dumpTable(table: String): List<List<Pair<String, String?>>> {
|
||||
fun dumpTable(table: String, columns: Set<String>?): List<List<Pair<String, String?>>> {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select()
|
||||
.from(table)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
val map: List<Pair<String, String?>> = 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 = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
val map: List<Pair<String, String?>> = cursor.columnNames.mapNotNull { column ->
|
||||
if (columns == null || columns.contains(column)) {
|
||||
val index = cursor.getColumnIndex(column)
|
||||
var data: String? = when (cursor.getType(index)) {
|
||||
Cursor.FIELD_TYPE_BLOB -> Hex.toStringCondensed(cursor.getBlob(index))
|
||||
else -> cursor.getString(index)
|
||||
}
|
||||
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
|
||||
column to data
|
||||
column to data
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ class UriAttachment : Attachment {
|
|||
transformProperties = transformProperties
|
||||
)
|
||||
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
dataUri: Uri,
|
||||
contentType: String,
|
||||
|
@ -64,7 +65,8 @@ class UriAttachment : Attachment {
|
|||
stickerLocator: StickerLocator?,
|
||||
blurHash: BlurHash?,
|
||||
audioHash: AudioHash?,
|
||||
transformProperties: TransformProperties?
|
||||
transformProperties: TransformProperties?,
|
||||
uuid: UUID? = UUID.randomUUID()
|
||||
) : super(
|
||||
contentType = contentType,
|
||||
transferState = transferState,
|
||||
|
@ -89,7 +91,7 @@ class UriAttachment : Attachment {
|
|||
blurHash = blurHash,
|
||||
audioHash = audioHash,
|
||||
transformProperties = transformProperties,
|
||||
uuid = UUID.randomUUID()
|
||||
uuid = uuid
|
||||
) {
|
||||
uri = Objects.requireNonNull(dataUri)
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
|||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
|
||||
import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messages
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.stickers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
|
||||
|
@ -655,6 +656,47 @@ class AttachmentTable(
|
|||
}
|
||||
}
|
||||
|
||||
fun deleteAttachments(toDelete: List<SyncAttachmentId>): List<SyncMessageId> {
|
||||
val unhandled = mutableListOf<SyncMessageId>()
|
||||
for (syncAttachmentId in toDelete) {
|
||||
val messageId = SignalDatabase.messages.getMessageIdOrNull(syncAttachmentId.syncMessageId)
|
||||
if (messageId != null) {
|
||||
val attachments = readableDatabase
|
||||
.select(ID, ATTACHMENT_UUID, REMOTE_DIGEST, DATA_HASH_END)
|
||||
.from(TABLE_NAME)
|
||||
.where("$MESSAGE_ID = ?", messageId)
|
||||
.run()
|
||||
.readToList {
|
||||
SyncAttachment(
|
||||
id = AttachmentId(it.requireLong(ID)),
|
||||
uuid = UuidUtil.parseOrNull(it.requireString(ATTACHMENT_UUID)),
|
||||
digest = it.requireBlob(REMOTE_DIGEST),
|
||||
plaintextHash = it.requireString(DATA_HASH_END)
|
||||
)
|
||||
}
|
||||
|
||||
val byUuid: SyncAttachment? by lazy { attachments.firstOrNull { it.uuid != null && it.uuid == syncAttachmentId.uuid } }
|
||||
val byDigest: SyncAttachment? by lazy { attachments.firstOrNull { it.digest != null && it.digest.contentEquals(syncAttachmentId.digest) } }
|
||||
val byPlaintext: SyncAttachment? by lazy { attachments.firstOrNull { it.plaintextHash != null && it.plaintextHash == syncAttachmentId.plaintextHash } }
|
||||
|
||||
val attachmentToDelete = (byUuid ?: byDigest ?: byPlaintext)?.id
|
||||
if (attachmentToDelete != null) {
|
||||
if (attachments.size == 1) {
|
||||
SignalDatabase.messages.deleteMessage(messageId)
|
||||
} else {
|
||||
deleteAttachment(attachmentToDelete)
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Unable to locate sync attachment to delete for message:$messageId")
|
||||
}
|
||||
} else {
|
||||
unhandled += syncAttachmentId.syncMessageId
|
||||
}
|
||||
}
|
||||
|
||||
return unhandled
|
||||
}
|
||||
|
||||
fun trimAllAbandonedAttachments() {
|
||||
val deleteCount = writableDatabase
|
||||
.delete(TABLE_NAME)
|
||||
|
@ -2295,4 +2337,8 @@ class AttachmentTable(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SyncAttachmentId(val syncMessageId: SyncMessageId, val uuid: UUID?, val digest: ByteArray?, val plaintextHash: String?)
|
||||
|
||||
class SyncAttachment(val id: AttachmentId, val uuid: UUID?, val digest: ByteArray?, val plaintextHash: String?)
|
||||
}
|
||||
|
|
|
@ -3465,6 +3465,15 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
}
|
||||
}
|
||||
|
||||
fun getMessageIdOrNull(message: SyncMessageId): Long? {
|
||||
return readableDatabase
|
||||
.select(ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$DATE_SENT = ? AND $FROM_RECIPIENT_ID = ?", message.timetamp, message.recipientId)
|
||||
.run()
|
||||
.readToSingleLongOrNull()
|
||||
}
|
||||
|
||||
fun deleteMessages(messagesToDelete: List<MessageTable.SyncMessageId>): List<SyncMessageId> {
|
||||
val threads = mutableSetOf<Long>()
|
||||
val unhandled = mutableListOf<SyncMessageId>()
|
||||
|
|
|
@ -8,8 +8,10 @@ package org.thoughtcrime.securesms.jobs
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
|
@ -17,12 +19,14 @@ import org.thoughtcrime.securesms.jobmanager.Job
|
|||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData
|
||||
import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData.AddressableMessage
|
||||
import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData.AttachmentDelete
|
||||
import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData.ThreadDelete
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.pad
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage.DeleteForMe
|
||||
|
@ -71,6 +75,26 @@ class MultiDeviceDeleteSendSyncJob private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@JvmStatic
|
||||
fun enqueueAttachmentDelete(message: MessageRecord?, attachment: DatabaseAttachment) {
|
||||
if (!TextSecurePreferences.isMultiDevice(AppDependencies.application)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!Recipient.self().deleteSyncCapability.isSupported) {
|
||||
Log.i(TAG, "Delete sync support not enabled.")
|
||||
return
|
||||
}
|
||||
|
||||
val delete = createAttachmentDelete(message, attachment)
|
||||
if (delete != null) {
|
||||
AppDependencies.jobManager.add(MultiDeviceDeleteSendSyncJob(attachments = listOf(delete)))
|
||||
} else {
|
||||
Log.i(TAG, "No valid attachment deletes to sync attachment:${attachment.attachmentId}")
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun enqueueThreadDeletes(threads: List<Pair<Long, Set<MessageRecord>>>, isFullDelete: Boolean) {
|
||||
if (!TextSecurePreferences.isMultiDevice(AppDependencies.application)) {
|
||||
|
@ -119,6 +143,48 @@ class MultiDeviceDeleteSendSyncJob private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun createAttachmentDelete(message: MessageRecord?, attachment: DatabaseAttachment): AttachmentDelete? {
|
||||
if (message == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(message.threadId)
|
||||
val addressableMessage = if (threadRecipient == null) {
|
||||
Log.w(TAG, "Unable to find thread recipient for message: ${message.id} thread: ${message.threadId} attachment: ${attachment.attachmentId}")
|
||||
null
|
||||
} else if (threadRecipient.isReleaseNotes) {
|
||||
Log.w(TAG, "Syncing release channel deletes are not currently supported")
|
||||
null
|
||||
} else if (threadRecipient.isDistributionList || !message.canDeleteSync()) {
|
||||
null
|
||||
} else {
|
||||
AddressableMessage(
|
||||
threadRecipientId = threadRecipient.id.toLong(),
|
||||
sentTimestamp = message.dateSent,
|
||||
authorRecipientId = message.fromRecipient.id.toLong()
|
||||
)
|
||||
}
|
||||
|
||||
if (addressableMessage == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val delete = AttachmentDelete(
|
||||
targetMessage = addressableMessage,
|
||||
uuid = attachment.uuid?.let { UuidUtil.toByteString(it) },
|
||||
digest = attachment.remoteDigest?.toByteString(),
|
||||
plaintextHash = attachment.dataHash?.let { Base64.decodeOrNull(it)?.toByteString() }
|
||||
)
|
||||
|
||||
return if (delete.uuid == null && delete.digest == null && delete.plaintextHash == null) {
|
||||
Log.w(TAG, "Unable to find uuid, digest, or plain text hash for attachment: ${attachment.attachmentId}")
|
||||
null
|
||||
} else {
|
||||
delete
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun createThreadDeletes(threads: List<Pair<Long, Set<MessageRecord>>>, isFullDelete: Boolean): List<ThreadDelete> {
|
||||
return threads.mapNotNull { (threadId, messages) ->
|
||||
|
@ -151,12 +217,14 @@ class MultiDeviceDeleteSendSyncJob private constructor(
|
|||
constructor(
|
||||
messages: List<AddressableMessage> = emptyList(),
|
||||
threads: List<ThreadDelete> = emptyList(),
|
||||
localOnlyThreads: List<ThreadDelete> = emptyList()
|
||||
localOnlyThreads: List<ThreadDelete> = emptyList(),
|
||||
attachments: List<AttachmentDelete> = emptyList()
|
||||
) : this(
|
||||
DeleteSyncJobData(
|
||||
messageDeletes = messages,
|
||||
threadDeletes = threads,
|
||||
localOnlyThreadDeletes = localOnlyThreads
|
||||
localOnlyThreadDeletes = localOnlyThreads,
|
||||
attachmentDeletes = attachments
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -244,13 +312,45 @@ class MultiDeviceDeleteSendSyncJob private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
if (data.attachmentDeletes.isNotEmpty()) {
|
||||
val success = syncDelete(
|
||||
DeleteForMe(
|
||||
attachmentDeletes = data.attachmentDeletes.mapNotNull {
|
||||
val conversation = Recipient.resolved(RecipientId.from(it.targetMessage!!.threadRecipientId)).toDeleteSyncConversationId()
|
||||
val targetMessage = it.targetMessage.toDeleteSyncMessage()
|
||||
|
||||
if (conversation != null && targetMessage != null) {
|
||||
DeleteForMe.AttachmentDelete(
|
||||
conversation = conversation,
|
||||
targetMessage = targetMessage,
|
||||
uuid = it.uuid,
|
||||
fallbackDigest = it.digest,
|
||||
fallbackPlaintextHash = it.plaintextHash
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "Unable to resolve ${it.targetMessage.threadRecipientId} to conversation id or resolve target message data")
|
||||
null
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (!success) {
|
||||
return Result.retry(defaultBackoff())
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
private fun syncDelete(deleteForMe: DeleteForMe): Boolean {
|
||||
if (deleteForMe.conversationDeletes.isEmpty() && deleteForMe.messageDeletes.isEmpty() && deleteForMe.localOnlyConversationDeletes.isEmpty()) {
|
||||
if (deleteForMe.conversationDeletes.isEmpty() &&
|
||||
deleteForMe.messageDeletes.isEmpty() &&
|
||||
deleteForMe.localOnlyConversationDeletes.isEmpty() &&
|
||||
deleteForMe.attachmentDeletes.isEmpty()
|
||||
) {
|
||||
Log.i(TAG, "No valid deletes, nothing to send, skipping")
|
||||
return true
|
||||
}
|
||||
|
@ -258,7 +358,7 @@ class MultiDeviceDeleteSendSyncJob private constructor(
|
|||
val syncMessageContent = deleteForMeContent(deleteForMe)
|
||||
|
||||
return try {
|
||||
Log.d(TAG, "Sending delete sync messageDeletes=${deleteForMe.messageDeletes.size} conversationDeletes=${deleteForMe.conversationDeletes.size} localOnlyConversationDeletes=${deleteForMe.localOnlyConversationDeletes.size}")
|
||||
Log.d(TAG, "Sending delete sync messageDeletes=${deleteForMe.messageDeletes.size} conversationDeletes=${deleteForMe.conversationDeletes.size} localOnlyConversationDeletes=${deleteForMe.localOnlyConversationDeletes.size} attachmentDeletes=${deleteForMe.attachmentDeletes.size}")
|
||||
AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessageContent, true, Optional.empty()).isSuccess
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Unable to send message delete sync", e)
|
||||
|
|
|
@ -4,6 +4,7 @@ import ProtoUtil.isNotEmpty
|
|||
import android.content.Context
|
||||
import com.mobilecoin.lib.exceptions.SerializationException
|
||||
import okio.ByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
|
@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.attachments.TombstoneAttachment
|
|||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
||||
|
@ -104,6 +106,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
|||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.EditMessage
|
||||
|
@ -1489,6 +1492,10 @@ object SyncMessageProcessor {
|
|||
handleSynchronizeLocalOnlyConversationDeletes(deleteForMe.localOnlyConversationDeletes, envelopeTimestamp)
|
||||
}
|
||||
|
||||
if (deleteForMe.attachmentDeletes.isNotEmpty()) {
|
||||
handleSynchronizeAttachmentDeletes(deleteForMe.attachmentDeletes, envelopeTimestamp, earlyMessageCacheEntry)
|
||||
}
|
||||
|
||||
AppDependencies.messageNotifier.updateNotification(context)
|
||||
}
|
||||
|
||||
|
@ -1570,6 +1577,26 @@ object SyncMessageProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleSynchronizeAttachmentDeletes(attachmentDeletes: List<SyncMessage.DeleteForMe.AttachmentDelete>, envelopeTimestamp: Long, earlyMessageCacheEntry: EarlyMessageCacheEntry?) {
|
||||
val toDelete: List<AttachmentTable.SyncAttachmentId> = attachmentDeletes
|
||||
.mapNotNull { delete ->
|
||||
delete.toSyncAttachmentId(delete.targetMessage?.toSyncMessageId(envelopeTimestamp), envelopeTimestamp)
|
||||
}
|
||||
|
||||
val unhandled: List<MessageTable.SyncMessageId> = SignalDatabase.attachments.deleteAttachments(toDelete)
|
||||
|
||||
for (syncMessage in unhandled) {
|
||||
warn(envelopeTimestamp, "[handleSynchronizeDeleteForMe] Could not find matching message for attachment delete! timestamp: ${syncMessage.timetamp} author: ${syncMessage.recipientId}")
|
||||
if (earlyMessageCacheEntry != null) {
|
||||
AppDependencies.earlyMessageCache.store(syncMessage.recipientId, syncMessage.timetamp, earlyMessageCacheEntry)
|
||||
}
|
||||
}
|
||||
|
||||
if (unhandled.isNotEmpty() && earlyMessageCacheEntry != null) {
|
||||
PushProcessEarlyMessagesJob.enqueue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun SyncMessage.DeleteForMe.ConversationIdentifier.toRecipientId(): RecipientId? {
|
||||
return when {
|
||||
threadGroupId != null -> {
|
||||
|
@ -1610,4 +1637,17 @@ object SyncMessageProcessor {
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun SyncMessage.DeleteForMe.AttachmentDelete.toSyncAttachmentId(syncMessageId: MessageTable.SyncMessageId?, envelopeTimestamp: Long): AttachmentTable.SyncAttachmentId? {
|
||||
val uuid = UuidUtil.fromByteStringOrNull(uuid)
|
||||
val digest = fallbackDigest?.toByteArray()
|
||||
val plaintextHash = fallbackPlaintextHash?.let { Base64.encodeWithPadding(it.toByteArray()) }
|
||||
|
||||
if (syncMessageId == null || (uuid == null && digest == null && plaintextHash == null)) {
|
||||
warn(envelopeTimestamp, "[handleSynchronizeDeleteForMe] Invalid delete sync attachment missing identifiers")
|
||||
return null
|
||||
} else {
|
||||
return AttachmentTable.SyncAttachmentId(syncMessageId, uuid, digest, plaintextHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
|||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraint;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSendSyncJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
|
@ -103,6 +104,9 @@ public class AttachmentUtil {
|
|||
SignalDatabase.messages().deleteMessage(mmsId);
|
||||
} else {
|
||||
SignalDatabase.attachments().deleteAttachment(attachmentId);
|
||||
if (Recipient.self().getDeleteSyncCapability().isSupported()) {
|
||||
MultiDeviceDeleteSendSyncJob.enqueueAttachmentDelete(SignalDatabase.messages().getMessageRecordOrNull(mmsId), attachment);
|
||||
}
|
||||
}
|
||||
|
||||
return deletedMessageRecord;
|
||||
|
|
|
@ -89,6 +89,13 @@ message DeleteSyncJobData {
|
|||
uint64 authorRecipientId = 3;
|
||||
}
|
||||
|
||||
message AttachmentDelete {
|
||||
AddressableMessage targetMessage = 1;
|
||||
optional bytes uuid = 2;
|
||||
optional bytes digest = 3;
|
||||
optional bytes plaintextHash = 4;
|
||||
}
|
||||
|
||||
message ThreadDelete {
|
||||
uint64 threadRecipientId = 1;
|
||||
repeated AddressableMessage messages = 2;
|
||||
|
@ -98,6 +105,7 @@ message DeleteSyncJobData {
|
|||
repeated AddressableMessage messageDeletes = 1;
|
||||
repeated ThreadDelete threadDeletes = 2;
|
||||
repeated ThreadDelete localOnlyThreadDeletes = 3;
|
||||
repeated AttachmentDelete attachmentDeletes = 4;
|
||||
}
|
||||
|
||||
message Svr3MirrorJobData {
|
||||
|
|
|
@ -676,6 +676,14 @@ message SyncMessage {
|
|||
repeated AddressableMessage messages = 2;
|
||||
}
|
||||
|
||||
message AttachmentDelete {
|
||||
optional ConversationIdentifier conversation = 1;
|
||||
optional AddressableMessage targetMessage = 2;
|
||||
optional bytes uuid = 3; // The `uuid` from the `Attachment`.
|
||||
optional bytes fallbackDigest = 4;
|
||||
optional bytes fallbackPlaintextHash = 5;
|
||||
}
|
||||
|
||||
message ConversationDelete {
|
||||
optional ConversationIdentifier conversation = 1;
|
||||
repeated AddressableMessage mostRecentMessages = 2;
|
||||
|
@ -689,6 +697,7 @@ message SyncMessage {
|
|||
repeated MessageDeletes messageDeletes = 1;
|
||||
repeated ConversationDelete conversationDeletes = 2;
|
||||
repeated LocalOnlyConversationDelete localOnlyConversationDeletes = 3;
|
||||
repeated AttachmentDelete attachmentDeletes = 4;
|
||||
}
|
||||
|
||||
optional Sent sent = 1;
|
||||
|
|
Loading…
Add table
Reference in a new issue