diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt index 0e2526d555..abe7a19bfa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt @@ -7,15 +7,19 @@ package org.thoughtcrime.securesms.backup.v2.database import android.database.Cursor import okio.ByteString.Companion.toByteString +import org.json.JSONArray +import org.json.JSONException import org.signal.core.util.Base64 import org.signal.core.util.Hex import org.signal.core.util.logging.Log +import org.signal.core.util.orNull import org.signal.core.util.requireBlob import org.signal.core.util.requireBoolean import org.signal.core.util.requireInt import org.signal.core.util.requireLong import org.signal.core.util.requireLongOrNull import org.signal.core.util.requireString +import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.Cdn import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.backup.v2.BackupRepository.getMediaName @@ -63,6 +67,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDet import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.mms.QuoteModel import org.thoughtcrime.securesms.payments.FailureReason import org.thoughtcrime.securesms.payments.State @@ -485,6 +490,52 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: } } + private fun BackupMessageRecord.parseLinkPreviews(attachments: List?): List { + if (linkPreview.isNullOrEmpty()) { + return emptyList() + } + val attachmentIdMap: Map = attachments?.associateBy { it.attachmentId } ?: emptyMap() + + try { + val previews: MutableList = LinkedList() + val jsonPreviews = JSONArray(linkPreview) + + for (i in 0 until jsonPreviews.length()) { + val preview = LinkPreview.deserialize(jsonPreviews.getJSONObject(i).toString()) + + if (preview.attachmentId != null) { + val attachment = attachmentIdMap[preview.attachmentId] + + if (attachment != null) { + previews += LinkPreview(preview.url, preview.title, preview.description, preview.date, attachment) + } else { + previews += preview + } + } else { + previews += preview + } + } + + return previews + } catch (e: JSONException) { + Log.w(TAG, "Failed to parse link preview", e) + } catch (e: IOException) { + Log.w(TAG, "Failed to parse shared contacts.", e) + } + + return emptyList() + } + + private fun LinkPreview.toBackupLinkPreview(): org.thoughtcrime.securesms.backup.v2.proto.LinkPreview { + return org.thoughtcrime.securesms.backup.v2.proto.LinkPreview( + url = url, + title = title, + image = (thumbnail.orNull() as? DatabaseAttachment)?.toBackupAttachment()?.pointer, + description = description, + date = date + ) + } + private fun BackupMessageRecord.toStandardMessage(reactionRecords: List?, mentions: List?, attachments: List?): StandardMessage { val text = if (body == null) { null @@ -494,14 +545,15 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: bodyRanges = (this.bodyRanges?.toBackupBodyRanges() ?: emptyList()) + (mentions?.toBackupBodyRanges() ?: emptyList()) ) } - val quotedAttachments = attachments?.filter { it.quote } ?: emptyList() - val messageAttachments = attachments?.filter { !it.quote } ?: emptyList() + val linkPreviews = this.parseLinkPreviews(attachments) + val linkPreviewAttachments = linkPreviews.mapNotNull { it.thumbnail.orElse(null) }.toSet() + val quotedAttachments = attachments?.filter { it.quote && !linkPreviewAttachments.contains(it) } ?: emptyList() + val messageAttachments = attachments?.filter { !it.quote && !linkPreviewAttachments.contains(it) } ?: emptyList() return StandardMessage( quote = this.toQuote(quotedAttachments), text = text, attachments = messageAttachments.toBackupAttachments(), - // TODO [backup] Link previews! - linkPreview = emptyList(), + linkPreview = linkPreviews.map { it.toBackupLinkPreview() }, longText = null, reactions = reactionRecords.toBackupReactions() ) @@ -845,6 +897,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: expireStarted = this.requireLong(MessageTable.EXPIRE_STARTED), remoteDeleted = this.requireBoolean(MessageTable.REMOTE_DELETED), sealedSender = this.requireBoolean(MessageTable.UNIDENTIFIED), + linkPreview = this.requireString(MessageTable.LINK_PREVIEWS), quoteTargetSentTimestamp = this.requireLong(MessageTable.QUOTE_ID), quoteAuthor = this.requireLong(MessageTable.QUOTE_AUTHOR), quoteBody = this.requireString(MessageTable.QUOTE_BODY), @@ -880,6 +933,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: val expireStarted: Long, val remoteDeleted: Boolean, val sealedSender: Boolean, + val linkPreview: String?, val quoteTargetSentTimestamp: Long, val quoteAuthor: Long, val quoteBody: String?, diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt index 1324a462b7..8901bfa929 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage import org.thoughtcrime.securesms.backup.v2.proto.FilePointer import org.thoughtcrime.securesms.backup.v2.proto.GroupCall import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall +import org.thoughtcrime.securesms.backup.v2.proto.LinkPreview import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification import org.thoughtcrime.securesms.backup.v2.proto.Quote @@ -335,15 +336,27 @@ class ChatItemImportInserter( } } } + val linkPreviews = this.standardMessage.linkPreview.map { it.toLocalLinkPreview() } + val linkPreviewAttachments = linkPreviews.mapNotNull { it.thumbnail.orNull() } val attachments = this.standardMessage.attachments.mapNotNull { attachment -> attachment.toLocalAttachment() } val quoteAttachments = this.standardMessage.quote?.attachments?.mapNotNull { it.toLocalAttachment() } ?: emptyList() - if (attachments.isNotEmpty()) { + if (attachments.isNotEmpty() || linkPreviewAttachments.isNotEmpty() || quoteAttachments.isNotEmpty()) { followUp = { messageRowId -> - SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, attachments, quoteAttachments) + val attachmentMap = SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, attachments + linkPreviewAttachments, quoteAttachments) + if (linkPreviews.isNotEmpty()) { + db.update( + MessageTable.TABLE_NAME, + contentValuesOf( + MessageTable.LINK_PREVIEWS to SignalDatabase.messages.getSerializedLinkPreviews(attachmentMap, linkPreviews) + ), + "${MessageTable.ID} = ?", + SqlUtil.buildArgs(messageRowId) + ) + } } } } @@ -930,6 +943,16 @@ class ChatItemImportInserter( ) } + private fun LinkPreview.toLocalLinkPreview(): org.thoughtcrime.securesms.linkpreview.LinkPreview { + return org.thoughtcrime.securesms.linkpreview.LinkPreview( + this.url, + this.title ?: "", + this.description ?: "", + this.date ?: 0, + Optional.ofNullable(this.image?.toLocalAttachment(voiceNote = false, borderless = false, gif = false, wasDownloaded = true)) + ) + } + private fun MessageAttachment.toLocalAttachment(): Attachment? { return pointer?.toLocalAttachment( voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE, diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableBackupExtensions.kt index f4a6b5cfce..5325135eaa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableBackupExtensions.kt @@ -33,6 +33,7 @@ fun MessageTable.getMessagesForBackup(backupTime: Long, archiveMedia: Boolean): MessageTable.EXPIRE_STARTED, MessageTable.REMOTE_DELETED, MessageTable.UNIDENTIFIED, + MessageTable.LINK_PREVIEWS, MessageTable.QUOTE_ID, MessageTable.QUOTE_AUTHOR, MessageTable.QUOTE_BODY, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index b9cc36e275..e0286f0928 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -3373,7 +3373,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat return sharedContactJson.toString() } - private fun getSerializedLinkPreviews(insertedAttachmentIds: Map, previews: List): String? { + fun getSerializedLinkPreviews(insertedAttachmentIds: Map, previews: List): String? { if (previews.isEmpty()) { return null }