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.audio.AudioHash
|
||||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
object UriAttachmentBuilder {
|
object UriAttachmentBuilder {
|
||||||
fun build(
|
fun build(
|
||||||
|
@ -22,23 +23,28 @@ object UriAttachmentBuilder {
|
||||||
stickerLocator: StickerLocator? = null,
|
stickerLocator: StickerLocator? = null,
|
||||||
blurHash: BlurHash? = null,
|
blurHash: BlurHash? = null,
|
||||||
audioHash: AudioHash? = null,
|
audioHash: AudioHash? = null,
|
||||||
transformProperties: AttachmentTable.TransformProperties? = null
|
transformProperties: AttachmentTable.TransformProperties? = null,
|
||||||
|
uuid: UUID? = UUID.randomUUID()
|
||||||
): UriAttachment {
|
): UriAttachment {
|
||||||
return UriAttachment(
|
return UriAttachment(
|
||||||
uri,
|
dataUri = uri,
|
||||||
contentType,
|
contentType = contentType,
|
||||||
transferState,
|
transferState = transferState,
|
||||||
size,
|
size = size,
|
||||||
fileName,
|
width = 0,
|
||||||
voiceNote,
|
height = 0,
|
||||||
borderless,
|
fileName = fileName,
|
||||||
videoGif,
|
fastPreflightId = null,
|
||||||
quote,
|
voiceNote = voiceNote,
|
||||||
caption,
|
borderless = borderless,
|
||||||
stickerLocator,
|
videoGif = videoGif,
|
||||||
blurHash,
|
quote = quote,
|
||||||
audioHash,
|
caption = caption,
|
||||||
transformProperties
|
stickerLocator = stickerLocator,
|
||||||
|
blurHash = blurHash,
|
||||||
|
audioHash = audioHash,
|
||||||
|
transformProperties = transformProperties,
|
||||||
|
uuid = uuid
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,23 +5,31 @@
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.messages
|
package org.thoughtcrime.securesms.messages
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
import io.mockk.unmockkStatic
|
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.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
||||||
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||||
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||||
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes inserting messages through the "normal" code paths simpler. Mostly focused on incoming messages.
|
* 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
|
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()
|
startTime = nextStartTime()
|
||||||
|
|
||||||
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
|
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,
|
sentTimeMillis = messageData.timestamp,
|
||||||
isUrgent = true,
|
isUrgent = true,
|
||||||
isSecure = true
|
isSecure = true
|
||||||
).apply { updateMessage?.invoke(this) }
|
).let { updateMessage?.invoke(it) ?: it }
|
||||||
|
|
||||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
||||||
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
|
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)
|
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 {
|
fun outgoingGroupChange(): MessageData {
|
||||||
startTime = nextStartTime()
|
startTime = nextStartTime()
|
||||||
|
|
||||||
|
@ -238,6 +260,20 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
||||||
return messageData
|
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.
|
* 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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.core.util.update
|
||||||
import org.signal.core.util.withinTransaction
|
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.CallTable
|
||||||
import org.thoughtcrime.securesms.database.MessageTable
|
import org.thoughtcrime.securesms.database.MessageTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
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.SignalActivityRule
|
||||||
import org.thoughtcrime.securesms.testing.assert
|
import org.thoughtcrime.securesms.testing.assert
|
||||||
import org.thoughtcrime.securesms.testing.assertIs
|
import org.thoughtcrime.securesms.testing.assertIs
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIsSize
|
||||||
import org.thoughtcrime.securesms.util.IdentityUtil
|
import org.thoughtcrime.securesms.util.IdentityUtil
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@Suppress("ClassName")
|
@Suppress("ClassName")
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@ -531,4 +538,154 @@ class SyncMessageProcessorTest_synchronizeDeleteForMe {
|
||||||
harness.inMemoryLogger.flush()
|
harness.inMemoryLogger.flush()
|
||||||
harness.inMemoryLogger.entries().filter { it.message?.contains("Thread is not local only") == true }.size assertIs 1
|
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
|
||||||
import okio.ByteString.Companion.toByteString
|
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.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||||
import org.thoughtcrime.securesms.messages.TestMessage
|
import org.thoughtcrime.securesms.messages.TestMessage
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
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.AttachmentPointer
|
||||||
import org.whispersystems.signalservice.internal.push.BodyRange
|
import org.whispersystems.signalservice.internal.push.BodyRange
|
||||||
import org.whispersystems.signalservice.internal.push.Content
|
import org.whispersystems.signalservice.internal.push.Content
|
||||||
|
@ -229,6 +232,35 @@ object MessageContentFuzzer {
|
||||||
).build()
|
).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:
|
* Create a random media message that may be:
|
||||||
* - A text body
|
* - A text body
|
||||||
|
@ -373,7 +405,8 @@ object MessageContentFuzzer {
|
||||||
data class DeleteForMeSync(
|
data class DeleteForMeSync(
|
||||||
val conversationId: RecipientId,
|
val conversationId: RecipientId,
|
||||||
val messages: List<Pair<RecipientId, Long>>,
|
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())
|
constructor(conversationId: RecipientId, vararg messages: Pair<RecipientId, Long>) : this(conversationId, messages.toList())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package org.thoughtcrime.securesms.testing
|
package org.thoughtcrime.securesms.testing
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.util.Base64
|
|
||||||
import org.hamcrest.Matcher
|
import org.hamcrest.Matcher
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
import org.hamcrest.Matchers.hasSize
|
import org.hamcrest.Matchers.hasSize
|
||||||
|
@ -9,6 +8,7 @@ import org.hamcrest.Matchers.`is`
|
||||||
import org.hamcrest.Matchers.not
|
import org.hamcrest.Matchers.not
|
||||||
import org.hamcrest.Matchers.notNullValue
|
import org.hamcrest.Matchers.notNullValue
|
||||||
import org.hamcrest.Matchers.nullValue
|
import org.hamcrest.Matchers.nullValue
|
||||||
|
import org.signal.core.util.Hex
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.readToList
|
import org.signal.core.util.readToList
|
||||||
import org.signal.core.util.select
|
import org.signal.core.util.select
|
||||||
|
@ -67,27 +67,31 @@ fun CountDownLatch.awaitFor(duration: Duration) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dumpTableToLogs(tag: String = "TestUtils", table: String) {
|
fun dumpTableToLogs(tag: String = "TestUtils", table: String, columns: Set<String>? = null) {
|
||||||
dumpTable(table).forEach { Log.d(tag, it.toString()) }
|
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
|
return SignalDatabase.rawDatabase
|
||||||
.select()
|
.select()
|
||||||
.from(table)
|
.from(table)
|
||||||
.run()
|
.run()
|
||||||
.readToList { cursor ->
|
.readToList { cursor ->
|
||||||
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
|
val map: List<Pair<String, String?>> = cursor.columnNames.mapNotNull { column ->
|
||||||
val index = cursor.getColumnIndex(column)
|
if (columns == null || columns.contains(column)) {
|
||||||
var data: String? = when (cursor.getType(index)) {
|
val index = cursor.getColumnIndex(column)
|
||||||
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
|
var data: String? = when (cursor.getType(index)) {
|
||||||
else -> cursor.getString(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))
|
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||||
}
|
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
||||||
|
}
|
||||||
|
|
||||||
column to data
|
column to data
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
map
|
map
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ class UriAttachment : Attachment {
|
||||||
transformProperties = transformProperties
|
transformProperties = transformProperties
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
constructor(
|
constructor(
|
||||||
dataUri: Uri,
|
dataUri: Uri,
|
||||||
contentType: String,
|
contentType: String,
|
||||||
|
@ -64,7 +65,8 @@ class UriAttachment : Attachment {
|
||||||
stickerLocator: StickerLocator?,
|
stickerLocator: StickerLocator?,
|
||||||
blurHash: BlurHash?,
|
blurHash: BlurHash?,
|
||||||
audioHash: AudioHash?,
|
audioHash: AudioHash?,
|
||||||
transformProperties: TransformProperties?
|
transformProperties: TransformProperties?,
|
||||||
|
uuid: UUID? = UUID.randomUUID()
|
||||||
) : super(
|
) : super(
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
transferState = transferState,
|
transferState = transferState,
|
||||||
|
@ -89,7 +91,7 @@ class UriAttachment : Attachment {
|
||||||
blurHash = blurHash,
|
blurHash = blurHash,
|
||||||
audioHash = audioHash,
|
audioHash = audioHash,
|
||||||
transformProperties = transformProperties,
|
transformProperties = transformProperties,
|
||||||
uuid = UUID.randomUUID()
|
uuid = uuid
|
||||||
) {
|
) {
|
||||||
uri = Objects.requireNonNull(dataUri)
|
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.ClassicDecryptingPartInputStream
|
||||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
||||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
|
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.messages
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.stickers
|
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.stickers
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
|
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() {
|
fun trimAllAbandonedAttachments() {
|
||||||
val deleteCount = writableDatabase
|
val deleteCount = writableDatabase
|
||||||
.delete(TABLE_NAME)
|
.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> {
|
fun deleteMessages(messagesToDelete: List<MessageTable.SyncMessageId>): List<SyncMessageId> {
|
||||||
val threads = mutableSetOf<Long>()
|
val threads = mutableSetOf<Long>()
|
||||||
val unhandled = mutableListOf<SyncMessageId>()
|
val unhandled = mutableListOf<SyncMessageId>()
|
||||||
|
|
|
@ -8,8 +8,10 @@ package org.thoughtcrime.securesms.jobs
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import okio.ByteString.Companion.toByteString
|
import okio.ByteString.Companion.toByteString
|
||||||
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.orNull
|
import org.signal.core.util.orNull
|
||||||
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
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.jobmanager.impl.NetworkConstraint
|
||||||
import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData
|
import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData
|
||||||
import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData.AddressableMessage
|
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.jobs.protos.DeleteSyncJobData.ThreadDelete
|
||||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.pad
|
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.pad
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException
|
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.Content
|
||||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||||
import org.whispersystems.signalservice.internal.push.SyncMessage.DeleteForMe
|
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
|
@WorkerThread
|
||||||
fun enqueueThreadDeletes(threads: List<Pair<Long, Set<MessageRecord>>>, isFullDelete: Boolean) {
|
fun enqueueThreadDeletes(threads: List<Pair<Long, Set<MessageRecord>>>, isFullDelete: Boolean) {
|
||||||
if (!TextSecurePreferences.isMultiDevice(AppDependencies.application)) {
|
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
|
@WorkerThread
|
||||||
private fun createThreadDeletes(threads: List<Pair<Long, Set<MessageRecord>>>, isFullDelete: Boolean): List<ThreadDelete> {
|
private fun createThreadDeletes(threads: List<Pair<Long, Set<MessageRecord>>>, isFullDelete: Boolean): List<ThreadDelete> {
|
||||||
return threads.mapNotNull { (threadId, messages) ->
|
return threads.mapNotNull { (threadId, messages) ->
|
||||||
|
@ -151,12 +217,14 @@ class MultiDeviceDeleteSendSyncJob private constructor(
|
||||||
constructor(
|
constructor(
|
||||||
messages: List<AddressableMessage> = emptyList(),
|
messages: List<AddressableMessage> = emptyList(),
|
||||||
threads: List<ThreadDelete> = emptyList(),
|
threads: List<ThreadDelete> = emptyList(),
|
||||||
localOnlyThreads: List<ThreadDelete> = emptyList()
|
localOnlyThreads: List<ThreadDelete> = emptyList(),
|
||||||
|
attachments: List<AttachmentDelete> = emptyList()
|
||||||
) : this(
|
) : this(
|
||||||
DeleteSyncJobData(
|
DeleteSyncJobData(
|
||||||
messageDeletes = messages,
|
messageDeletes = messages,
|
||||||
threadDeletes = threads,
|
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()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure() = Unit
|
override fun onFailure() = Unit
|
||||||
|
|
||||||
private fun syncDelete(deleteForMe: DeleteForMe): Boolean {
|
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")
|
Log.i(TAG, "No valid deletes, nothing to send, skipping")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -258,7 +358,7 @@ class MultiDeviceDeleteSendSyncJob private constructor(
|
||||||
val syncMessageContent = deleteForMeContent(deleteForMe)
|
val syncMessageContent = deleteForMeContent(deleteForMe)
|
||||||
|
|
||||||
return try {
|
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
|
AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessageContent, true, Optional.empty()).isSuccess
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, "Unable to send message delete sync", e)
|
Log.w(TAG, "Unable to send message delete sync", e)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import ProtoUtil.isNotEmpty
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.mobilecoin.lib.exceptions.SerializationException
|
import com.mobilecoin.lib.exceptions.SerializationException
|
||||||
import okio.ByteString
|
import okio.ByteString
|
||||||
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.Hex
|
import org.signal.core.util.Hex
|
||||||
import org.signal.core.util.orNull
|
import org.signal.core.util.orNull
|
||||||
import org.signal.libsignal.protocol.IdentityKey
|
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.components.emoji.EmojiUtil
|
||||||
import org.thoughtcrime.securesms.contactshare.Contact
|
import org.thoughtcrime.securesms.contactshare.Contact
|
||||||
import org.thoughtcrime.securesms.crypto.SecurityEvent
|
import org.thoughtcrime.securesms.crypto.SecurityEvent
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||||
import org.thoughtcrime.securesms.database.CallTable
|
import org.thoughtcrime.securesms.database.CallTable
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
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.ServiceId.PNI
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
import org.whispersystems.signalservice.api.storage.StorageKey
|
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.Content
|
||||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||||
import org.whispersystems.signalservice.internal.push.EditMessage
|
import org.whispersystems.signalservice.internal.push.EditMessage
|
||||||
|
@ -1489,6 +1492,10 @@ object SyncMessageProcessor {
|
||||||
handleSynchronizeLocalOnlyConversationDeletes(deleteForMe.localOnlyConversationDeletes, envelopeTimestamp)
|
handleSynchronizeLocalOnlyConversationDeletes(deleteForMe.localOnlyConversationDeletes, envelopeTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deleteForMe.attachmentDeletes.isNotEmpty()) {
|
||||||
|
handleSynchronizeAttachmentDeletes(deleteForMe.attachmentDeletes, envelopeTimestamp, earlyMessageCacheEntry)
|
||||||
|
}
|
||||||
|
|
||||||
AppDependencies.messageNotifier.updateNotification(context)
|
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? {
|
private fun SyncMessage.DeleteForMe.ConversationIdentifier.toRecipientId(): RecipientId? {
|
||||||
return when {
|
return when {
|
||||||
threadGroupId != null -> {
|
threadGroupId != null -> {
|
||||||
|
@ -1610,4 +1637,17 @@ object SyncMessageProcessor {
|
||||||
null
|
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.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraint;
|
import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraint;
|
||||||
|
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSendSyncJob;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -103,6 +104,9 @@ public class AttachmentUtil {
|
||||||
SignalDatabase.messages().deleteMessage(mmsId);
|
SignalDatabase.messages().deleteMessage(mmsId);
|
||||||
} else {
|
} else {
|
||||||
SignalDatabase.attachments().deleteAttachment(attachmentId);
|
SignalDatabase.attachments().deleteAttachment(attachmentId);
|
||||||
|
if (Recipient.self().getDeleteSyncCapability().isSupported()) {
|
||||||
|
MultiDeviceDeleteSendSyncJob.enqueueAttachmentDelete(SignalDatabase.messages().getMessageRecordOrNull(mmsId), attachment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return deletedMessageRecord;
|
return deletedMessageRecord;
|
||||||
|
|
|
@ -89,6 +89,13 @@ message DeleteSyncJobData {
|
||||||
uint64 authorRecipientId = 3;
|
uint64 authorRecipientId = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message AttachmentDelete {
|
||||||
|
AddressableMessage targetMessage = 1;
|
||||||
|
optional bytes uuid = 2;
|
||||||
|
optional bytes digest = 3;
|
||||||
|
optional bytes plaintextHash = 4;
|
||||||
|
}
|
||||||
|
|
||||||
message ThreadDelete {
|
message ThreadDelete {
|
||||||
uint64 threadRecipientId = 1;
|
uint64 threadRecipientId = 1;
|
||||||
repeated AddressableMessage messages = 2;
|
repeated AddressableMessage messages = 2;
|
||||||
|
@ -98,6 +105,7 @@ message DeleteSyncJobData {
|
||||||
repeated AddressableMessage messageDeletes = 1;
|
repeated AddressableMessage messageDeletes = 1;
|
||||||
repeated ThreadDelete threadDeletes = 2;
|
repeated ThreadDelete threadDeletes = 2;
|
||||||
repeated ThreadDelete localOnlyThreadDeletes = 3;
|
repeated ThreadDelete localOnlyThreadDeletes = 3;
|
||||||
|
repeated AttachmentDelete attachmentDeletes = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Svr3MirrorJobData {
|
message Svr3MirrorJobData {
|
||||||
|
|
|
@ -676,6 +676,14 @@ message SyncMessage {
|
||||||
repeated AddressableMessage messages = 2;
|
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 {
|
message ConversationDelete {
|
||||||
optional ConversationIdentifier conversation = 1;
|
optional ConversationIdentifier conversation = 1;
|
||||||
repeated AddressableMessage mostRecentMessages = 2;
|
repeated AddressableMessage mostRecentMessages = 2;
|
||||||
|
@ -689,6 +697,7 @@ message SyncMessage {
|
||||||
repeated MessageDeletes messageDeletes = 1;
|
repeated MessageDeletes messageDeletes = 1;
|
||||||
repeated ConversationDelete conversationDeletes = 2;
|
repeated ConversationDelete conversationDeletes = 2;
|
||||||
repeated LocalOnlyConversationDelete localOnlyConversationDeletes = 3;
|
repeated LocalOnlyConversationDelete localOnlyConversationDeletes = 3;
|
||||||
|
repeated AttachmentDelete attachmentDeletes = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional Sent sent = 1;
|
optional Sent sent = 1;
|
||||||
|
|
Loading…
Add table
Reference in a new issue