Add import/export for stickers and sticker packs.

This commit is contained in:
Clark
2024-06-12 18:47:25 -04:00
committed by Greyson Parrelli
parent 11557e4815
commit 75b41c34ea
7 changed files with 241 additions and 59 deletions

View File

@@ -10,6 +10,7 @@ import android.os.Parcel
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.stickers.StickerLocator
class ArchivedAttachment : Attachment {
@@ -44,6 +45,7 @@ class ArchivedAttachment : Attachment {
blurHash: String?,
voiceNote: Boolean,
borderless: Boolean,
stickerLocator: StickerLocator?,
gif: Boolean,
quote: Boolean
) : super(
@@ -66,7 +68,7 @@ class ArchivedAttachment : Attachment {
incrementalMacChunkSize = incrementalMacChunkSize ?: 0,
uploadTimestamp = 0,
caption = caption,
stickerLocator = null,
stickerLocator = stickerLocator,
blurHash = BlurHash.parseOrNull(blurHash),
audioHash = null,
transformProperties = null

View File

@@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.StickerBackupProcessor
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
@@ -129,6 +130,11 @@ object BackupRepository {
eventTimer.emit("call")
}
StickerBackupProcessor.export { frame ->
writer.write(frame)
eventTimer.emit("sticker-pack")
}
ChatItemBackupProcessor.export(exportState) { frame ->
writer.write(frame)
eventTimer.emit("message")
@@ -186,6 +192,7 @@ object BackupRepository {
SignalDatabase.threads.clearAllDataForBackupRestore()
SignalDatabase.messages.clearAllDataForBackupRestore()
SignalDatabase.attachments.clearAllDataForBackupRestore()
SignalDatabase.stickers.clearAllDataForBackupRestore()
// Add back self after clearing data
val selfId: RecipientId = SignalDatabase.recipients.getAndPossiblyMerge(selfData.aci, selfData.pni, selfData.e164, pniVerified = true, changeSelf = true)
@@ -222,6 +229,11 @@ object BackupRepository {
eventTimer.emit("call")
}
frame.stickerPack != null -> {
StickerBackupProcessor.import(frame.stickerPack)
eventTimer.emit("sticker-pack")
}
frame.chatItem != null -> {
chatItemInserter.insert(frame.chatItem)
eventTimer.emit("chatItem")

View File

@@ -10,6 +10,7 @@ import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.Base64.decode
import org.signal.core.util.Base64.decodeOrThrow
import org.signal.core.util.Hex
import org.signal.core.util.logging.Log
import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean
@@ -37,6 +38,8 @@ import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
import org.thoughtcrime.securesms.backup.v2.proto.SessionSwitchoverChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
import org.thoughtcrime.securesms.backup.v2.proto.Sticker
import org.thoughtcrime.securesms.backup.v2.proto.StickerMessage
import org.thoughtcrime.securesms.backup.v2.proto.Text
import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate
import org.thoughtcrime.securesms.database.AttachmentTable
@@ -84,7 +87,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange
*
* All of this complexity is hidden from the user -- they just get a normal iterator interface.
*/
class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: Int, private val archiveMedia: Boolean) : Iterator<ChatItem>, Closeable {
class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: Int, private val archiveMedia: Boolean) : Iterator<ChatItem?>, Closeable {
companion object {
private val TAG = Log.tag(ChatItemExportIterator::class.java)
@@ -104,7 +107,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
return buffer.isNotEmpty() || (cursor.count > 0 && !cursor.isLast && !cursor.isAfterLast)
}
override fun next(): ChatItem {
override fun next(): ChatItem? {
if (buffer.isNotEmpty()) {
return buffer.remove()
}
@@ -344,11 +347,31 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
)
}
}
record.body == null && !attachmentsById.containsKey(record.id) -> {
Log.w(TAG, "Record missing a body and doesnt have attachments, skipping")
continue
else -> {
if (record.body == null && !attachmentsById.containsKey(record.id)) {
Log.w(TAG, "Record missing a body and doesnt have attachments, skipping")
continue
}
val attachments = attachmentsById[record.id]
val sticker = attachments?.firstOrNull { dbAttachment ->
dbAttachment.isSticker
}
if (sticker != null) {
val stickerLocator = sticker.stickerLocator!!
builder.stickerMessage = StickerMessage(
sticker = Sticker(
packId = Hex.fromStringCondensed(stickerLocator.packId).toByteString(),
packKey = Hex.fromStringCondensed(stickerLocator.packKey).toByteString(),
stickerId = stickerLocator.stickerId,
emoji = stickerLocator.emoji,
data_ = sticker.toBackupAttachment().pointer
),
reactions = reactionsById[id].toBackupReactions()
)
} else {
builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id])
}
}
else -> builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id])
}
if (record.latestRevisionId == null) {
val previousEdits = revisionMap.remove(record.id)
@@ -369,7 +392,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
return if (buffer.isNotEmpty()) {
buffer.remove()
} else {
throw NoSuchElementException()
null
}
}

View File

@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.backup.v2.database
import android.content.ContentValues
import androidx.core.content.contentValuesOf
import org.signal.core.util.Base64
import org.signal.core.util.Hex
import org.signal.core.util.SqlUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
@@ -22,6 +23,7 @@ import org.thoughtcrime.securesms.backup.v2.BackupState
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.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
@@ -31,6 +33,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.Reaction
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
import org.thoughtcrime.securesms.backup.v2.proto.Sticker
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.GroupReceiptTable
@@ -61,6 +64,7 @@ import org.thoughtcrime.securesms.payments.State
import org.thoughtcrime.securesms.payments.proto.PaymentMetaData
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
@@ -340,6 +344,15 @@ class ChatItemImportInserter(
}
}
}
if (this.stickerMessage != null) {
val sticker = this.stickerMessage.sticker
val attachment = sticker.toLocalAttachment()
if (attachment != null) {
followUp = { messageRowId ->
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(attachment), emptyList())
}
}
}
return MessageInsert(contentValues, followUp)
}
@@ -804,74 +817,114 @@ class ChatItemImportInserter(
}
}
private fun MessageAttachment.toLocalAttachment(contentType: String? = pointer?.contentType, fileName: String? = pointer?.fileName): Attachment? {
if (pointer == null) return null
if (pointer.attachmentLocator != null) {
private fun FilePointer?.toLocalAttachment(voiceNote: Boolean, borderless: Boolean, gif: Boolean, wasDownloaded: Boolean, stickerLocator: StickerLocator? = null, contentType: String? = this?.contentType, fileName: String? = this?.fileName): Attachment? {
if (this == null) return null
if (attachmentLocator != null) {
val signalAttachmentPointer = SignalServiceAttachmentPointer(
pointer.attachmentLocator.cdnNumber,
SignalServiceAttachmentRemoteId.from(pointer.attachmentLocator.cdnKey),
attachmentLocator.cdnNumber,
SignalServiceAttachmentRemoteId.from(attachmentLocator.cdnKey),
contentType,
pointer.attachmentLocator.key.toByteArray(),
Optional.ofNullable(pointer.attachmentLocator.size),
attachmentLocator.key.toByteArray(),
Optional.ofNullable(attachmentLocator.size),
Optional.empty(),
pointer.width ?: 0,
pointer.height ?: 0,
Optional.ofNullable(pointer.attachmentLocator.digest.toByteArray()),
Optional.ofNullable(pointer.incrementalMac?.toByteArray()),
pointer.incrementalMacChunkSize ?: 0,
width ?: 0,
height ?: 0,
Optional.ofNullable(attachmentLocator.digest.toByteArray()),
Optional.ofNullable(incrementalMac?.toByteArray()),
incrementalMacChunkSize ?: 0,
Optional.ofNullable(fileName),
flag == MessageAttachment.Flag.VOICE_MESSAGE,
flag == MessageAttachment.Flag.BORDERLESS,
flag == MessageAttachment.Flag.GIF,
Optional.ofNullable(pointer.caption),
Optional.ofNullable(pointer.blurHash),
pointer.attachmentLocator.uploadTimestamp
voiceNote,
borderless,
gif,
Optional.ofNullable(caption),
Optional.ofNullable(blurHash),
attachmentLocator.uploadTimestamp
)
return PointerAttachment.forPointer(
pointer = Optional.of(signalAttachmentPointer),
stickerLocator = stickerLocator,
transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING
).orNull()
} else if (pointer.invalidAttachmentLocator != null) {
} else if (invalidAttachmentLocator != null) {
return TombstoneAttachment(
contentType = contentType,
incrementalMac = pointer.incrementalMac?.toByteArray(),
incrementalMacChunkSize = pointer.incrementalMacChunkSize,
width = pointer.width,
height = pointer.height,
caption = pointer.caption,
blurHash = pointer.blurHash,
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
gif = flag == MessageAttachment.Flag.GIF,
incrementalMac = incrementalMac?.toByteArray(),
incrementalMacChunkSize = incrementalMacChunkSize,
width = width,
height = height,
caption = caption,
blurHash = blurHash,
voiceNote = voiceNote,
borderless = borderless,
gif = gif,
quote = false
)
} else if (pointer.backupLocator != null) {
} else if (backupLocator != null) {
return ArchivedAttachment(
contentType = contentType,
size = pointer.backupLocator.size.toLong(),
cdn = pointer.backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
key = pointer.backupLocator.key.toByteArray(),
cdnKey = pointer.backupLocator.transitCdnKey,
archiveCdn = pointer.backupLocator.cdnNumber,
archiveMediaName = pointer.backupLocator.mediaName,
archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(pointer.backupLocator.mediaName)).encode(),
archiveThumbnailMediaId = backupState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(pointer.backupLocator.mediaName)).encode(),
digest = pointer.backupLocator.digest.toByteArray(),
incrementalMac = pointer.incrementalMac?.toByteArray(),
incrementalMacChunkSize = pointer.incrementalMacChunkSize,
width = pointer.width,
height = pointer.height,
caption = pointer.caption,
blurHash = pointer.blurHash,
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
gif = flag == MessageAttachment.Flag.GIF,
quote = false
size = backupLocator.size.toLong(),
cdn = backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
key = backupLocator.key.toByteArray(),
cdnKey = backupLocator.transitCdnKey,
archiveCdn = backupLocator.cdnNumber,
archiveMediaName = backupLocator.mediaName,
archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(backupLocator.mediaName)).encode(),
archiveThumbnailMediaId = backupState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(backupLocator.mediaName)).encode(),
digest = backupLocator.digest.toByteArray(),
incrementalMac = incrementalMac?.toByteArray(),
incrementalMacChunkSize = incrementalMacChunkSize,
width = width,
height = height,
caption = caption,
blurHash = blurHash,
voiceNote = voiceNote,
borderless = borderless,
gif = gif,
quote = false,
stickerLocator = stickerLocator
)
}
return null
}
private fun Sticker?.toLocalAttachment(): Attachment? {
if (this == null) return null
return data_.toLocalAttachment(
voiceNote = false,
gif = false,
borderless = false,
wasDownloaded = true,
stickerLocator = StickerLocator(
packId = Hex.toStringCondensed(packId.toByteArray()),
packKey = Hex.toStringCondensed(packKey.toByteArray()),
stickerId = stickerId,
emoji = emoji
)
)
}
private fun MessageAttachment.toLocalAttachment(): Attachment? {
return pointer?.toLocalAttachment(
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
gif = flag == MessageAttachment.Flag.GIF,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
wasDownloaded = wasDownloaded
)
}
private fun MessageAttachment.toLocalAttachment(contentType: String?, fileName: String?): Attachment? {
return pointer?.toLocalAttachment(
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
gif = flag == MessageAttachment.Flag.GIF,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
wasDownloaded = wasDownloaded,
contentType = contentType,
fileName = fileName
)
}
private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? {
return thumbnail?.toLocalAttachment(this.contentType, this.fileName)
?: if (this.contentType == null) null else PointerAttachment.forPointer(quotedAttachment = DataMessage.Quote.QuotedAttachment(contentType = this.contentType, fileName = this.fileName, thumbnail = null)).orNull()

View File

@@ -0,0 +1,13 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.database.StickerTable
fun StickerTable.clearAllDataForBackupRestore() {
writableDatabase.deleteAll(StickerTable.TABLE_NAME)
}

View File

@@ -20,9 +20,12 @@ object ChatItemBackupProcessor {
fun export(exportState: ExportState, emitter: BackupFrameEmitter) {
SignalDatabase.messages.getMessagesForBackup(exportState.backupTime, exportState.allowMediaBackup).use { chatItems ->
for (chatItem in chatItems) {
if (exportState.threadIds.contains(chatItem.chatId)) {
emitter.emit(Frame(chatItem = chatItem))
while (chatItems.hasNext()) {
val chatItem = chatItems.next()
if (chatItem != null) {
if (exportState.threadIds.contains(chatItem.chatId)) {
emitter.emit(Frame(chatItem = chatItem))
}
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.processor
import androidx.annotation.WorkerThread
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Hex
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.proto.StickerPack
import org.thoughtcrime.securesms.backup.v2.proto.StickerPackSticker
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.StickerTable.StickerPackRecordReader
import org.thoughtcrime.securesms.database.StickerTable.StickerRecordReader
import org.thoughtcrime.securesms.database.model.StickerPackRecord
import org.thoughtcrime.securesms.database.model.StickerRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob
object StickerBackupProcessor {
fun export(emitter: BackupFrameEmitter) {
StickerPackRecordReader(SignalDatabase.stickers.allStickerPacks).use { reader ->
var record: StickerPackRecord? = reader.next
while (record != null) {
if (record.isInstalled) {
val frame = record.toBackupFrame()
emitter.emit(frame)
}
record = reader.next
}
}
}
fun import(stickerPack: StickerPack) {
AppDependencies.jobManager.add(
StickerPackDownloadJob.forInstall(Hex.toStringCondensed(stickerPack.packId.toByteArray()), Hex.toStringCondensed(stickerPack.packKey.toByteArray()), false)
)
}
}
@WorkerThread
private fun getStickersFromDatabase(packId: String): List<StickerPackSticker> {
val stickers: MutableList<StickerPackSticker> = java.util.ArrayList()
SignalDatabase.stickers.getStickersForPack(packId).use { cursor ->
val reader = StickerRecordReader(cursor)
var record: StickerRecord? = reader.next
while (record != null) {
stickers.add(
StickerPackSticker(
emoji = record.emoji,
id = record.stickerId
)
)
record = reader.next
}
}
return stickers
}
private fun StickerPackRecord.toBackupFrame(): Frame {
val packIdBytes = Hex.fromStringCondensed(packId)
val packKey = Hex.fromStringCondensed(packKey)
val stickers = getStickersFromDatabase(packId)
val pack = StickerPack(
packId = packIdBytes.toByteString(),
packKey = packKey.toByteString(),
title = title.orElse(""),
author = author.orElse(""),
stickers = stickers
)
return Frame(stickerPack = pack)
}