Add single attachment delete sync.

This commit is contained in:
Cody Henthorne
2024-06-18 10:02:03 -04:00
committed by Greyson Parrelli
parent ea87108def
commit 09003d85b1
13 changed files with 492 additions and 38 deletions

View File

@@ -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)
}

View File

@@ -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?)
}

View File

@@ -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>()

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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;