Support pinned messages in backups.

This commit is contained in:
Michelle Tang
2025-12-16 11:35:39 -05:00
committed by jeffrey-signal
parent b99fec4274
commit b65079ec20
26 changed files with 124 additions and 23 deletions

View File

@@ -139,6 +139,10 @@ object ExportSkips {
return log(sentTimestamp, "Poll was not in a group chat.")
}
fun pinMessageIsInvalid(sentTimestamp: Long): String {
return log(sentTimestamp, "Pin message update was invalid.")
}
fun individualChatUpdateInWrongTypeOfChat(sentTimestamp: Long): String {
return log(sentTimestamp, "A chat update that only makes sense for individual chats was found in a different kind of chat.")
}

View File

@@ -65,7 +65,10 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
${MessageTable.MISMATCHED_IDENTITIES},
${MessageTable.TYPE},
${MessageTable.MESSAGE_EXTRAS},
${MessageTable.VIEW_ONCE}
${MessageTable.VIEW_ONCE},
${MessageTable.PINNED_UNTIL},
${MessageTable.PINNING_MESSAGE_ID},
${MessageTable.PINNED_AT}
)
WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1
""".trimMargin()
@@ -155,7 +158,10 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
MessageTable.TYPE,
MessageTable.MESSAGE_EXTRAS,
MessageTable.VIEW_ONCE,
PARENT_STORY_ID
PARENT_STORY_ID,
MessageTable.PINNED_UNTIL,
MessageTable.PINNING_MESSAGE_ID,
MessageTable.PINNED_AT
)
.from("${MessageTable.TABLE_NAME} INDEXED BY $dateReceivedIndex")
.where("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND ($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds}) AND $DATE_RECEIVED >= $lastSeenReceivedTime $cutoffQuery")

View File

@@ -9,6 +9,7 @@ import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.json.JSONArray
import org.json.JSONException
import org.signal.core.models.ServiceId
import org.signal.core.util.Base64
import org.signal.core.util.EventTimer
import org.signal.core.util.Hex
@@ -53,6 +54,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.LearnedProfileChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification
import org.thoughtcrime.securesms.backup.v2.proto.PinMessageUpdate
import org.thoughtcrime.securesms.backup.v2.proto.Poll
import org.thoughtcrime.securesms.backup.v2.proto.PollTerminateUpdate
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
@@ -413,6 +415,16 @@ class ChatItemArchiveExporter(
transformTimer.emit("poll")
}
MessageTypes.isPinnedMessageUpdate(record.type) -> {
val pinMessageUpdate = record.toRemotePinMessageUpdate(exportState)
if (pinMessageUpdate == null) {
Log.w(TAG, ExportSkips.pinMessageIsInvalid(record.dateSent))
continue
}
builder.updateMessage = ChatUpdateMessage(pinMessage = pinMessageUpdate)
transformTimer.emit("pin-message")
}
else -> {
val attachments = extraData.attachmentsById[record.id]
val sticker = attachments?.firstOrNull { dbAttachment -> dbAttachment.isSticker }
@@ -595,6 +607,7 @@ private fun BackupMessageRecord.toBasicChatItemBuilder(selfRecipientId: Recipien
expiresInMs = record.expiresIn.takeIf { it > 0 }
revisions = emptyList()
sms = record.type.isSmsType()
pinDetails = record.toPinDetails()
when (direction) {
Direction.DIRECTIONLESS -> {
directionless = ChatItem.DirectionlessMessageDetails()
@@ -847,6 +860,27 @@ private fun BackupMessageRecord.toRemotePollTerminateUpdate(): PollTerminateUpda
)
}
private fun BackupMessageRecord.toPinDetails(): ChatItem.PinDetails? {
return if (this.pinnedAt == 0L || this.pinnedUntil == 0L) {
null
} else {
ChatItem.PinDetails(
pinnedAtTimestamp = this.pinnedAt,
pinExpiresAtTimestamp = this.pinnedUntil.takeIf { it != MessageTable.PIN_FOREVER },
pinNeverExpires = (this.pinnedUntil == MessageTable.PIN_FOREVER).takeIf { it }
)
}
}
private fun BackupMessageRecord.toRemotePinMessageUpdate(exportState: ExportState): PinMessageUpdate? {
val pinMessage = this.messageExtras?.pinnedMessage ?: return null
val authorId = exportState.aciToRecipientId[ServiceId.ACI.parseOrNull(pinMessage.targetAuthorAci).toString()] ?: return null
return PinMessageUpdate(
targetSentTimestamp = pinMessage.targetTimestamp,
authorId = authorId
)
}
private fun BackupMessageRecord.toRemoteSharedContact(attachments: List<DatabaseAttachment>?): Contact? {
if (this.sharedContacts.isNullOrEmpty()) {
return null
@@ -1591,7 +1625,8 @@ private fun Long.isDirectionlessType(): Boolean {
MessageTypes.isGroupUpdate(this) ||
MessageTypes.isGroupV1MigrationEvent(this) ||
MessageTypes.isGroupQuit(this) ||
MessageTypes.isPollTerminate(this)
MessageTypes.isPollTerminate(this) ||
MessageTypes.isPinnedMessageUpdate(this)
}
private fun Long.isIdentityVerifyType(): Boolean {
@@ -1776,6 +1811,8 @@ private fun Cursor.toBackupMessageRecord(pastIds: Set<Long>, backupStartTime: Lo
messageExtras = messageExtras.parseMessageExtras(),
viewOnce = this.requireBoolean(MessageTable.VIEW_ONCE),
parentStoryId = this.requireLong(MessageTable.PARENT_STORY_ID),
pinnedAt = this.requireLong(MessageTable.PINNED_AT),
pinnedUntil = this.requireLong(MessageTable.PINNED_UNTIL),
messageExtrasSize = messageExtras?.size ?: 0
)
}
@@ -1816,6 +1853,8 @@ private class BackupMessageRecord(
val baseType: Long,
val messageExtras: MessageExtras?,
val viewOnce: Boolean,
val pinnedAt: Long,
val pinnedUntil: Long,
private val messageExtrasSize: Int
) {
val estimatedSizeInBytes: Int = (body?.length ?: 0) +

View File

@@ -64,6 +64,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescrip
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
import org.thoughtcrime.securesms.database.model.databaseprotos.PaymentTombstone
import org.thoughtcrime.securesms.database.model.databaseprotos.PinnedMessage
import org.thoughtcrime.securesms.database.model.databaseprotos.PollTerminate
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
@@ -137,7 +138,10 @@ class ChatItemArchiveImporter(
MessageTable.LATEST_REVISION_ID,
MessageTable.REVISION_NUMBER,
MessageTable.PARENT_STORY_ID,
MessageTable.NOTIFIED
MessageTable.NOTIFIED,
MessageTable.PINNED_UNTIL,
MessageTable.PINNING_MESSAGE_ID,
MessageTable.PINNED_AT
)
private val REACTION_COLUMNS = arrayOf(
@@ -343,6 +347,32 @@ class ChatItemArchiveImporter(
SignalDatabase.polls.endPoll(pollId = pollId, endingMessageId = endPollMessageId)
}
}
} else if (this.updateMessage.pinMessage != null) {
followUps += { pinUpdateMessageId ->
val targetAuthorId = importState.remoteToLocalRecipientId[updateMessage.pinMessage.authorId]
if (targetAuthorId != null) {
val pinnedMessageId = SignalDatabase.messages.getMessageFor(updateMessage.pinMessage.targetSentTimestamp, targetAuthorId)?.id ?: -1
val messageExtras = MessageExtras(
pinnedMessage = PinnedMessage(
pinnedMessageId = pinnedMessageId,
targetAuthorAci = recipients.getRecord(targetAuthorId).aci!!.toByteString(),
targetTimestamp = updateMessage.pinMessage.targetSentTimestamp
)
)
db.update(MessageTable.TABLE_NAME)
.values(MessageTable.MESSAGE_EXTRAS to messageExtras.encode())
.where("${MessageTable.ID} = ?", pinUpdateMessageId)
.run()
if (pinnedMessageId != -1L) {
db.update(MessageTable.TABLE_NAME)
.values(MessageTable.PINNING_MESSAGE_ID to pinUpdateMessageId)
.where("${MessageTable.ID} = ?", pinnedMessageId)
.run()
}
}
}
}
}
@@ -645,6 +675,12 @@ class ChatItemArchiveImporter(
contentValues.put(MessageTable.REMOTE_DELETED, 0)
contentValues.put(MessageTable.PARENT_STORY_ID, 0)
if (this.pinDetails != null) {
val pinnedUntil = if (this.pinDetails.pinNeverExpires == true) MessageTable.PIN_FOREVER else this.pinDetails.pinExpiresAtTimestamp
contentValues.put(MessageTable.PINNED_UNTIL, pinnedUntil ?: 0)
contentValues.put(MessageTable.PINNED_AT, this.pinDetails.pinnedAtTimestamp)
}
when {
this.standardMessage != null -> contentValues.addStandardMessage(this.standardMessage)
this.remoteDeletedMessage != null -> contentValues.put(MessageTable.REMOTE_DELETED, 1)
@@ -846,6 +882,9 @@ class ChatItemArchiveImporter(
updateMessage.pollTerminate != null -> {
typeFlags = MessageTypes.SPECIAL_TYPE_POLL_TERMINATE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
}
updateMessage.pinMessage != null -> {
typeFlags = MessageTypes.SPECIAL_TYPE_PINNED_MESSAGE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
}
updateMessage.sessionSwitchover != null -> {
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
val sessionSwitchoverDetails = SessionSwitchoverEvent(e164 = updateMessage.sessionSwitchover.e164.toString()).encode()