Add small system for consolidating archive export errors.
This commit is contained in:
parent
039bebb30c
commit
8777c1ff89
2 changed files with 111 additions and 28 deletions
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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<DatabaseAttachment>?): List<Contact> {
|
||||
private fun BackupMessageRecord.toRemoteSharedContacts(attachments: List<DatabaseAttachment>?): List<Contact>? {
|
||||
if (this.sharedContacts.isNullOrEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val attachmentIdMap: Map<AttachmentId, DatabaseAttachment> = attachments?.associateBy { it.attachmentId } ?: emptyMap()
|
||||
|
||||
try {
|
||||
return try {
|
||||
val contacts: MutableList<Contact> = LinkedList()
|
||||
val jsonContacts = JSONArray(sharedContacts)
|
||||
|
||||
|
@ -699,14 +702,14 @@ private fun BackupMessageRecord.toRemoteSharedContacts(attachments: List<Databas
|
|||
}
|
||||
}
|
||||
|
||||
return contacts
|
||||
contacts
|
||||
} catch (e: JSONException) {
|
||||
Log.w(TAG, "Failed to parse shared contacts.", e)
|
||||
Log.w(TAG, ExportSkips.failedToParseSharedContact(this.dateSent), e)
|
||||
null
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to parse shared contacts.", e)
|
||||
Log.w(TAG, ExportSkips.failedToParseSharedContact(this.dateSent), e)
|
||||
null
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toRemoteLinkPreviews(attachments: List<DatabaseAttachment>?): List<LinkPreview> {
|
||||
|
@ -737,9 +740,9 @@ private fun BackupMessageRecord.toRemoteLinkPreviews(attachments: List<DatabaseA
|
|||
|
||||
return previews
|
||||
} catch (e: JSONException) {
|
||||
Log.w(TAG, "Failed to parse link preview", e)
|
||||
Log.w(TAG, ExportOddities.failedToParseLinkPreview(this.dateSent), e)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to parse shared contacts.", e)
|
||||
Log.w(TAG, ExportOddities.failedToParseLinkPreview(this.dateSent), e)
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
|
@ -764,8 +767,8 @@ private fun BackupMessageRecord.toRemoteViewOnceMessage(mediaArchiveEnabled: Boo
|
|||
)
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toRemoteContactMessage(mediaArchiveEnabled: Boolean, reactionRecords: List<ReactionRecord>?, attachments: List<DatabaseAttachment>?): ContactMessage {
|
||||
val sharedContacts = toRemoteSharedContacts(attachments)
|
||||
private fun BackupMessageRecord.toRemoteContactMessage(mediaArchiveEnabled: Boolean, reactionRecords: List<ReactionRecord>?, attachments: List<DatabaseAttachment>?): 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<Mention>.toRemoteBodyRanges(db: SignalDatabase): List<BackupBod
|
|||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.toRemoteBodyRanges(): List<BackupBodyRange> {
|
||||
private fun ByteArray.toRemoteBodyRanges(dateSent: Long): List<BackupBodyRange> {
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue