mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 20:55:10 +00:00
Add single attachment delete sync.
This commit is contained in:
committed by
Greyson Parrelli
parent
ea87108def
commit
09003d85b1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user