Add backup support for DirectStoryReplyMessages.

This commit is contained in:
Greyson Parrelli
2025-01-16 11:04:18 -05:00
parent adda6f9ba8
commit 1459dbf64d
21 changed files with 109 additions and 19 deletions

View File

@@ -76,6 +76,11 @@ class ArchiveImportExportTests {
runTests { it.startsWith("chat_item_contact_message_") }
}
// @Test
fun chatItemDirectStoryReplyMessage() {
runTests { it.startsWith("chat_item_direct_story_reply_") }
}
// @Test
fun chatItemExpirationTimerUpdate() {
runTests { it.startsWith("chat_item_expiration_timer_update_") }

View File

@@ -43,6 +43,10 @@ object ExportSkips {
return log(sentTimestamp, "Group update record is parseable, but has no updates.")
}
fun directStoryReplyHasNoBody(sentTimestamp: Long): String {
return log(sentTimestamp, "Direct story reply has no body.")
}
private fun log(sentTimestamp: Long, message: String): String {
return "[SKIP][$sentTimestamp] $message"
}

View File

@@ -59,7 +59,8 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, medi
${MessageTable.MISMATCHED_IDENTITIES},
${MessageTable.TYPE},
${MessageTable.MESSAGE_EXTRAS},
${MessageTable.VIEW_ONCE}
${MessageTable.VIEW_ONCE},
${MessageTable.PARENT_STORY_ID}
)
""".trimMargin()
)
@@ -123,7 +124,8 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, medi
MessageTable.MISMATCHED_IDENTITIES,
MessageTable.TYPE,
MessageTable.MESSAGE_EXTRAS,
MessageTable.VIEW_ONCE
MessageTable.VIEW_ONCE,
MessageTable.PARENT_STORY_ID
)
.from("${MessageTable.TABLE_NAME} INDEXED BY $dateReceivedIndex")
.where("${MessageTable.STORY_TYPE} = 0 AND ${MessageTable.DATE_RECEIVED} >= $lastSeenReceivedTime")

View File

@@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment
import org.thoughtcrime.securesms.backup.v2.proto.ContactMessage
import org.thoughtcrime.securesms.backup.v2.proto.DirectStoryReplyMessage
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GenericGroupUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
@@ -291,6 +292,10 @@ class ChatItemArchiveExporter(
builder.viewOnceMessage = record.toRemoteViewOnceMessage(mediaArchiveEnabled = mediaArchiveEnabled, reactionRecords = extraData.reactionsById[id], attachments = extraData.attachmentsById[id])
}
record.parentStoryId != 0L -> {
builder.directStoryReplyMessage = record.toRemoteDirectStoryReplyMessage(mediaArchiveEnabled = mediaArchiveEnabled, reactionRecords = extraData.reactionsById[id], attachments = extraData.attachmentsById[record.id]) ?: continue
}
else -> {
if (record.body.isNullOrEmpty() && !extraData.attachmentsById.containsKey(record.id)) {
Log.w(TAG, ExportSkips.emptyChatItem(record.dateSent))
@@ -852,6 +857,36 @@ private fun Contact.PostalAddress.Type.toRemote(): ContactAttachment.PostalAddre
}
}
private fun BackupMessageRecord.toRemoteDirectStoryReplyMessage(mediaArchiveEnabled: Boolean, reactionRecords: List<ReactionRecord>?, attachments: List<DatabaseAttachment>?): DirectStoryReplyMessage? {
if (this.body.isNullOrBlank()) {
Log.w(TAG, ExportSkips.directStoryReplyHasNoBody(this.dateSent))
return null
}
val isReaction = MessageTypes.isStoryReaction(this.type)
return DirectStoryReplyMessage(
storySentTimestamp = this.parentStoryId.takeUnless { it == MessageTable.PARENT_STORY_MISSING_ID },
emoji = if (isReaction) {
this.body
} else {
null
},
textReply = if (!isReaction) {
DirectStoryReplyMessage.TextReply(
text = Text(
body = this.body,
bodyRanges = this.bodyRanges?.toRemoteBodyRanges(this.dateSent) ?: emptyList()
),
longText = attachments?.firstOrNull { it.contentType == MediaUtil.LONG_TEXT }?.toRemoteFilePointer(mediaArchiveEnabled)
)
} else {
null
},
reactions = reactionRecords.toRemote()
)
}
private fun BackupMessageRecord.toRemoteStandardMessage(db: SignalDatabase, mediaArchiveEnabled: Boolean, reactionRecords: List<ReactionRecord>?, mentions: List<Mention>?, attachments: List<DatabaseAttachment>?): StandardMessage {
val text = body?.let {
Text(
@@ -1344,7 +1379,8 @@ private fun Cursor.toBackupMessageRecord(pastIds: Set<Long>, backupStartTime: Lo
identityMismatchRecipientIds = this.requireString(MessageTable.MISMATCHED_IDENTITIES).parseIdentityMismatches(),
baseType = this.requireLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK,
messageExtras = this.requireBlob(MessageTable.MESSAGE_EXTRAS).parseMessageExtras(),
viewOnce = this.requireBoolean(MessageTable.VIEW_ONCE)
viewOnce = this.requireBoolean(MessageTable.VIEW_ONCE),
parentStoryId = this.requireLong(MessageTable.PARENT_STORY_ID)
)
}
@@ -1372,6 +1408,7 @@ private class BackupMessageRecord(
val quoteBodyRanges: ByteArray?,
val quoteType: Int,
val originalMessageId: Long?,
val parentStoryId: Long,
val latestRevisionId: Long?,
val hasDeliveryReceipt: Boolean,
val hasReadReceipt: Boolean,

View File

@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment
import org.thoughtcrime.securesms.backup.v2.proto.DirectStoryReplyMessage
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.LinkPreview
@@ -125,7 +126,8 @@ class ChatItemArchiveImporter(
MessageTable.VIEW_ONCE,
MessageTable.MESSAGE_EXTRAS,
MessageTable.ORIGINAL_MESSAGE_ID,
MessageTable.LATEST_REVISION_ID
MessageTable.LATEST_REVISION_ID,
MessageTable.PARENT_STORY_ID
)
private val REACTION_COLUMNS = arrayOf(
@@ -238,10 +240,11 @@ class ChatItemArchiveImporter(
private fun ChatItem.toMessageInsert(fromRecipientId: RecipientId, chatRecipientId: RecipientId, threadId: Long): MessageInsert {
val contentValues = this.toMessageContentValues(fromRecipientId, chatRecipientId, threadId)
var followUp: ((Long) -> Unit)? = null
val followUps: MutableList<(Long) -> Unit> = mutableListOf()
if (this.updateMessage != null) {
if (this.updateMessage.individualCall != null && this.updateMessage.individualCall.callId != null) {
followUp = { messageRowId ->
followUps += { messageRowId ->
val values = contentValuesOf(
CallTable.CALL_ID to updateMessage.individualCall.callId,
CallTable.MESSAGE_ID to messageRowId,
@@ -263,7 +266,7 @@ class ChatItemArchiveImporter(
db.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
}
} else if (this.updateMessage.groupCall != null && this.updateMessage.groupCall.callId != null) {
followUp = { messageRowId ->
followUps += { messageRowId ->
val ringer: RecipientId? = this.updateMessage.groupCall.ringerRecipientId?.let { importState.remoteToLocalRecipientId[it] }
val values = contentValuesOf(
@@ -295,7 +298,7 @@ class ChatItemArchiveImporter(
}
if (this.paymentNotification != null) {
followUp = { messageRowId ->
followUps += { messageRowId ->
val uuid = tryRestorePayment(this, chatRecipientId)
if (uuid != null) {
db.update(MessageTable.TABLE_NAME)
@@ -347,7 +350,7 @@ class ChatItemArchiveImporter(
if (contact != null) {
val contactAttachment: Attachment? = contact.avatarAttachment
followUp = { messageRowId ->
followUps += { messageRowId ->
val attachmentMap = if (contactAttachment != null) {
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(contactAttachment), emptyList())
} else {
@@ -365,6 +368,19 @@ class ChatItemArchiveImporter(
}
}
if (this.directStoryReplyMessage != null) {
val longTextAttachment: Attachment? = this.directStoryReplyMessage.textReply?.longText?.toLocalAttachment(
importState = importState,
contentType = "text/x-signal-plain"
)
if (longTextAttachment != null) {
followUps += { messageRowId ->
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(longTextAttachment), emptyList())
}
}
}
if (this.standardMessage != null) {
val bodyRanges = this.standardMessage.text?.bodyRanges
if (!bodyRanges.isNullOrEmpty()) {
@@ -380,7 +396,7 @@ class ChatItemArchiveImporter(
}
}
if (mentions.isNotEmpty()) {
followUp = { messageId ->
followUps += { messageId ->
SignalDatabase.mentions.insert(threadId, messageId, mentions)
}
}
@@ -391,19 +407,17 @@ class ChatItemArchiveImporter(
attachment.toLocalAttachment()
}
val longTextAttachments: List<Attachment> = this.standardMessage.longText?.let { longTextPointer ->
longTextPointer.toLocalAttachment(
importState = importState,
contentType = "text/x-signal-plain"
)
}?.let { listOf(it) } ?: emptyList()
val longTextAttachments: List<Attachment> = this.standardMessage.longText?.toLocalAttachment(
importState = importState,
contentType = "text/x-signal-plain"
)?.let { listOf(it) } ?: emptyList()
val quoteAttachments: List<Attachment> = this.standardMessage.quote?.toLocalAttachments() ?: emptyList()
val hasAttachments = attachments.isNotEmpty() || linkPreviewAttachments.isNotEmpty() || quoteAttachments.isNotEmpty() || longTextAttachments.isNotEmpty()
if (hasAttachments || linkPreviews.isNotEmpty()) {
followUp = { messageRowId ->
followUps += { messageRowId ->
val attachmentMap = if (hasAttachments) {
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, attachments + linkPreviewAttachments + longTextAttachments, quoteAttachments)
} else {
@@ -424,7 +438,7 @@ class ChatItemArchiveImporter(
val sticker = this.stickerMessage.sticker
val attachment = sticker.toLocalAttachment()
if (attachment != null) {
followUp = { messageRowId ->
followUps += { messageRowId ->
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(attachment), emptyList())
}
}
@@ -433,12 +447,20 @@ class ChatItemArchiveImporter(
if (this.viewOnceMessage != null) {
val attachment = this.viewOnceMessage.attachment?.toLocalAttachment()
if (attachment != null) {
followUp = { messageRowId ->
followUps += { messageRowId ->
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(attachment), emptyList())
}
}
}
val followUp: ((Long) -> Unit)? = if (followUps.isNotEmpty()) {
{ messageId ->
followUps.forEach { it(messageId) }
}
} else {
null
}
return MessageInsert(contentValues, followUp)
}
@@ -505,6 +527,7 @@ class ChatItemArchiveImporter(
this.paymentNotification != null -> contentValues.addPaymentNotification(this, chatRecipientId)
this.giftBadge != null -> contentValues.addGiftBadge(this.giftBadge)
this.viewOnceMessage != null -> contentValues.addViewOnce(this.viewOnceMessage)
this.directStoryReplyMessage != null -> contentValues.addDirectStoryReply(this.directStoryReplyMessage)
}
return contentValues
@@ -548,6 +571,7 @@ class ChatItemArchiveImporter(
this.contactMessage != null -> this.contactMessage.reactions
this.stickerMessage != null -> this.stickerMessage.reactions
this.viewOnceMessage != null -> this.viewOnceMessage.reactions
this.directStoryReplyMessage != null -> this.directStoryReplyMessage.reactions
else -> emptyList()
}
@@ -625,6 +649,10 @@ class ChatItemArchiveImporter(
type = type or MessageTypes.SPECIAL_TYPE_GIFT_BADGE
}
if (this.directStoryReplyMessage?.emoji != null) {
type = type or MessageTypes.SPECIAL_TYPE_STORY_REACTION
}
return type
}
@@ -848,6 +876,19 @@ class ChatItemArchiveImporter(
put(MessageTable.VIEW_ONCE, true.toInt())
}
private fun ContentValues.addDirectStoryReply(directStoryReply: DirectStoryReplyMessage) {
put(MessageTable.PARENT_STORY_ID, directStoryReply.storySentTimestamp?.takeUnless { it == 0L } ?: MessageTable.PARENT_STORY_MISSING_ID)
if (directStoryReply.emoji != null) {
put(MessageTable.BODY, directStoryReply.emoji)
}
if (directStoryReply.textReply != null) {
put(MessageTable.BODY, directStoryReply.textReply.text?.body)
put(MessageTable.MESSAGE_RANGES, directStoryReply.textReply.text?.bodyRanges?.toLocalBodyRanges()?.encode())
}
}
private fun String?.tryParseMoney(): Money? {
if (this.isNullOrEmpty()) {
return null

View File

@@ -210,6 +210,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
const val QUOTE_TARGET_MISSING_ID = -1L
const val ADDRESSABLE_MESSAGE_LIMIT = 5
const val PARENT_STORY_MISSING_ID = -1L
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (