From 8777c1ff89344163089fd67181a37f37732f0ade Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 15 Jan 2025 10:17:46 -0500 Subject: [PATCH] Add small system for consolidating archive export errors. --- .../securesms/backup/v2/ArchiveErrorCases.kt | 80 +++++++++++++++++++ .../v2/exporters/ChatItemArchiveExporter.kt | 59 +++++++------- 2 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt new file mode 100644 index 0000000000..005bcffdea --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2 + +import org.thoughtcrime.securesms.database.CallTable + +/** + * These represent situations where we will skip exporting a data frame due to the data being invalid. + */ +object ExportSkips { + fun emptyChatItem(sentTimestamp: Long): String { + return log(sentTimestamp, "Completely empty ChatItem (no body or attachments).") + } + + fun invalidLongTextChatItem(sentTimestamp: Long): String { + return log(sentTimestamp, "ChatItem with a long-text attachment had no body.") + } + + fun messageExpiresTooSoon(sentTimestamp: Long): String { + return log(sentTimestamp, "Message expires too soon. Must skip.") + } + + fun individualCallStateNotMappable(sentTimestamp: Long, event: CallTable.Event): String { + return log(sentTimestamp, "Unable to map group only status to 1:1 call state. Event: ${event.name}") + } + + fun failedToParseSharedContact(sentTimestamp: Long): String { + return log(sentTimestamp, "Failed to parse shared contacts.") + } + + fun failedToParseGiftBadge(sentTimestamp: Long): String { + return log(sentTimestamp, "Failed to parse GiftBadge.") + } + + fun failedToParseGroupUpdate(sentTimestamp: Long): String { + return log(sentTimestamp, "Failed to parse GroupUpdate.") + } + + fun groupUpdateHasNoUpdates(sentTimestamp: Long): String { + return log(sentTimestamp, "Group update record is parseable, but has no updates.") + } + + private fun log(sentTimestamp: Long, message: String): String { + return "[SKIP][$sentTimestamp] $message" + } +} + +/** + * These represent situations where we encounter some weird data, but are still able to export the frame. We may have needed to "massage" the data to get + * it to fit the spec. + */ +object ExportOddities { + + fun revisionsOnNonStandardMessage(sentTimestamp: Long): String { + return log(sentTimestamp, "Attempted to set revisions on a non-standard message. Ignoring revisions.") + } + + fun outgoingMessageWasSentButTimerNotStarted(sentTimestamp: Long): String { + return log(sentTimestamp, "Outgoing expiring message was sent, but the timer wasn't started. Setting expireStartDate to dateReceived.") + } + + fun incomingMessageWasReadButTimerNotStarted(sentTimestamp: Long): String { + return log(sentTimestamp, "Incoming expiring message was read, but the timer wasn't started. Setting expireStartDate to dateReceived.") + } + + fun failedToParseBodyRangeList(sentTimestamp: Long): String { + return log(sentTimestamp, "Unable to parse BodyRangeList. Ignoring it.") + } + + fun failedToParseLinkPreview(sentTimestamp: Long): String { + return log(sentTimestamp, "Failed to parse link preview. Ignoring it.") + } + + private fun log(sentTimestamp: Long, message: String): String { + return "[ODDITY][$sentTimestamp] $message" + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt index a1b59f5e0a..6a1deb8cbc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt @@ -24,6 +24,8 @@ import org.signal.core.util.requireLongOrNull import org.signal.core.util.requireString import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.DatabaseAttachment +import org.thoughtcrime.securesms.backup.v2.ExportOddities +import org.thoughtcrime.securesms.backup.v2.ExportSkips import org.thoughtcrime.securesms.backup.v2.ExportState import org.thoughtcrime.securesms.backup.v2.database.getThreadGroupStatus import org.thoughtcrime.securesms.backup.v2.proto.ChatItem @@ -250,7 +252,7 @@ class ChatItemArchiveExporter( MessageTypes.isGroupV2(record.type) && MessageTypes.isGroupUpdate(record.type) -> { val update = record.toRemoteGroupUpdate() ?: continue if (update.groupChange!!.updates.isEmpty()) { - Log.w(TAG, "Group update record with ID ${record.id} missing updates. Skipping.") + Log.w(TAG, ExportSkips.groupUpdateHasNoUpdates(record.dateSent)) continue } builder.updateMessage = update @@ -278,11 +280,11 @@ class ChatItemArchiveExporter( } MessageTypes.isGiftBadge(record.type) -> { - builder.giftBadge = record.toRemoteGiftBadgeUpdate() + builder.giftBadge = record.toRemoteGiftBadgeUpdate() ?: continue } !record.sharedContacts.isNullOrEmpty() -> { - builder.contactMessage = record.toRemoteContactMessage(mediaArchiveEnabled = mediaArchiveEnabled, reactionRecords = extraData.reactionsById[id], attachments = extraData.attachmentsById[id]) + builder.contactMessage = record.toRemoteContactMessage(mediaArchiveEnabled = mediaArchiveEnabled, reactionRecords = extraData.reactionsById[id], attachments = extraData.attachmentsById[id]) ?: continue } record.viewOnce -> { @@ -291,13 +293,13 @@ class ChatItemArchiveExporter( else -> { if (record.body.isNullOrEmpty() && !extraData.attachmentsById.containsKey(record.id)) { - Log.w(TAG, "Record with ID ${record.id} missing a body and doesn't have attachments. Skipping.") + Log.w(TAG, ExportSkips.emptyChatItem(record.dateSent)) continue } val attachments = extraData.attachmentsById[record.id] if (attachments?.isNotEmpty() == true && attachments.all { it.contentType == MediaUtil.LONG_TEXT } && record.body.isNullOrEmpty()) { - Log.w(TAG, "Record with ID ${record.id} has long text attachments, but no body. Skipping.") + Log.w(TAG, ExportSkips.invalidLongTextChatItem(record.dateSent)) continue } @@ -323,7 +325,7 @@ class ChatItemArchiveExporter( if (builder.standardMessage != null) { builder.revisions = previousEdits } else { - Log.w(TAG, "[${record.dateSent}] Attempted to set revisions on a non-standard message! Ignoring.") + Log.w(TAG, ExportOddities.revisionsOnNonStandardMessage(record.dateSent)) } } buffer += builder.build() @@ -452,7 +454,7 @@ private fun BackupMessageRecord.toBasicChatItemBuilder(selfRecipientId: Recipien ) if (expiresInMs != null && outgoing?.sendStatus?.all { it.pending == null && it.failed == null } == true) { - Log.w(TAG, "Outgoing expiring message was sent but the timer wasn't started! Fixing.") + Log.w(TAG, ExportOddities.outgoingMessageWasSentButTimerNotStarted(record.dateSent)) expireStartDate = record.dateReceived } } @@ -465,7 +467,7 @@ private fun BackupMessageRecord.toBasicChatItemBuilder(selfRecipientId: Recipien ) if (expiresInMs != null && incoming?.read == true && expireStartDate == null) { - Log.w(TAG, "Incoming expiring message was read but the timer wasn't started! Fixing.") + Log.w(TAG, ExportOddities.incomingMessageWasReadButTimerNotStarted(record.dateSent)) expireStartDate = record.dateReceived } } @@ -473,7 +475,7 @@ private fun BackupMessageRecord.toBasicChatItemBuilder(selfRecipientId: Recipien } if (!MessageTypes.isExpirationTimerUpdate(record.type) && builder.expiresInMs != null && builder.expireStartDate != null && builder.expireStartDate!! + builder.expiresInMs!! < backupStartTime + 1.days.inWholeMilliseconds) { - Log.w(TAG, "Message expires too soon! Must skip.") + Log.w(TAG, ExportSkips.messageExpiresTooSoon(record.dateSent)) return null } @@ -548,6 +550,7 @@ private fun BackupMessageRecord.toRemoteGroupUpdate(): ChatUpdateMessage? { groupChange = GroupsV2UpdateMessageConverter.translateDecryptedChange(selfIds = SignalStore.account.getServiceIds(), context) ) } catch (e: IOException) { + Log.w(TAG, ExportSkips.failedToParseGroupUpdate(this.dateSent), e) null } } @@ -636,7 +639,7 @@ private fun CallTable.Call.toRemoteCallUpdate(db: SignalDatabase, messageRecord: CallTable.Event.GENERIC_GROUP_CALL, CallTable.Event.RINGING, CallTable.Event.OUTGOING_RING -> { - Log.w(TAG, "Unable to map group only status to 1:1 call state, skipping. event: ${this.event.name}") + Log.w(TAG, ExportSkips.individualCallStateNotMappable(messageRecord.dateSent, this.event)) return null } }, @@ -670,14 +673,14 @@ private fun BackupMessageRecord.toRemotePaymentNotificationUpdate(db: SignalData } } -private fun BackupMessageRecord.toRemoteSharedContacts(attachments: List?): List { +private fun BackupMessageRecord.toRemoteSharedContacts(attachments: List?): List? { if (this.sharedContacts.isNullOrEmpty()) { return emptyList() } val attachmentIdMap: Map = attachments?.associateBy { it.attachmentId } ?: emptyMap() - try { + return try { val contacts: MutableList = LinkedList() val jsonContacts = JSONArray(sharedContacts) @@ -699,14 +702,14 @@ private fun BackupMessageRecord.toRemoteSharedContacts(attachments: List?): List { @@ -737,9 +740,9 @@ private fun BackupMessageRecord.toRemoteLinkPreviews(attachments: List?, attachments: List?): ContactMessage { - val sharedContacts = toRemoteSharedContacts(attachments) +private fun BackupMessageRecord.toRemoteContactMessage(mediaArchiveEnabled: Boolean, reactionRecords: List?, attachments: List?): ContactMessage? { + val sharedContacts = toRemoteSharedContacts(attachments) ?: return null val contacts = sharedContacts.map { ContactAttachment( @@ -858,7 +861,7 @@ private fun BackupMessageRecord.toRemoteStandardMessage(db: SignalDatabase, medi val text = body?.let { Text( body = it, - bodyRanges = (this.bodyRanges?.toRemoteBodyRanges() ?: emptyList()) + (mentions?.toRemoteBodyRanges(db) ?: emptyList()) + bodyRanges = (this.bodyRanges?.toRemoteBodyRanges(this.dateSent) ?: emptyList()) + (mentions?.toRemoteBodyRanges(db) ?: emptyList()) ) } @@ -905,7 +908,7 @@ private fun BackupMessageRecord.toRemoteQuote(mediaArchiveEnabled: Boolean, atta text = this.quoteBody?.let { body -> Text( body = body, - bodyRanges = this.quoteBodyRanges?.toRemoteBodyRanges() ?: emptyList() + bodyRanges = this.quoteBodyRanges?.toRemoteBodyRanges(this.dateSent) ?: emptyList() ) }, attachments = if (remoteType == Quote.Type.VIEW_ONCE) { @@ -917,12 +920,12 @@ private fun BackupMessageRecord.toRemoteQuote(mediaArchiveEnabled: Boolean, atta ) } -private fun BackupMessageRecord.toRemoteGiftBadgeUpdate(): BackupGiftBadge { +private fun BackupMessageRecord.toRemoteGiftBadgeUpdate(): BackupGiftBadge? { val giftBadge = try { GiftBadge.ADAPTER.decode(Base64.decode(this.body ?: "")) } catch (e: IOException) { - Log.w(TAG, "Failed to decode GiftBadge!") - return BackupGiftBadge() + Log.w(TAG, ExportSkips.failedToParseGiftBadge(this.dateSent), e) + return null } return BackupGiftBadge( @@ -1040,11 +1043,11 @@ private fun List.toRemoteBodyRanges(db: SignalDatabase): List { +private fun ByteArray.toRemoteBodyRanges(dateSent: Long): List { val decoded: BodyRangeList = try { BodyRangeList.ADAPTER.decode(this) } catch (e: IOException) { - Log.w(TAG, "Failed to decode BodyRangeList!") + Log.w(TAG, ExportOddities.failedToParseBodyRangeList(dateSent), e) return emptyList() }