mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Support pinned messages in backups.
This commit is contained in:
committed by
jeffrey-signal
parent
b99fec4274
commit
b65079ec20
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -216,6 +216,11 @@ class ArchiveImportExportTests {
|
||||
runTests { it.startsWith("chat_item_poll_") }
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun chatItemPinMessage() {
|
||||
runTests { it.startsWith("chat_item_pin_message_") }
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun notificationProfiles() {
|
||||
runTests { it.startsWith("notification_profile_") }
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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) +
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -460,6 +460,14 @@ message ChatItem {
|
||||
message DirectionlessMessageDetails {
|
||||
}
|
||||
|
||||
message PinDetails {
|
||||
uint64 pinnedAtTimestamp = 1;
|
||||
oneof pinExpiry {
|
||||
uint64 pinExpiresAtTimestamp = 2; // timestamp when the pin should expire
|
||||
bool pinNeverExpires = 3;
|
||||
}
|
||||
}
|
||||
|
||||
uint64 chatId = 1; // conversation id
|
||||
uint64 authorId = 2; // recipient id
|
||||
uint64 dateSent = 3;
|
||||
@@ -488,6 +496,8 @@ message ChatItem {
|
||||
DirectStoryReplyMessage directStoryReplyMessage = 19; // group story reply messages are not backed up
|
||||
Poll poll = 20;
|
||||
}
|
||||
|
||||
PinDetails pinDetails = 21; // only set if message is pinned
|
||||
}
|
||||
|
||||
message SendStatus {
|
||||
@@ -898,6 +908,7 @@ message ChatUpdateMessage {
|
||||
GroupCall groupCall = 8;
|
||||
LearnedProfileChatUpdate learnedProfileChange = 9;
|
||||
PollTerminateUpdate pollTerminate = 10;
|
||||
PinMessageUpdate pinMessage = 11;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1268,6 +1279,11 @@ message PollTerminateUpdate {
|
||||
string question = 2; // Between 1-100 characters
|
||||
}
|
||||
|
||||
message PinMessageUpdate {
|
||||
uint64 targetSentTimestamp = 1;
|
||||
uint64 authorId = 2; // recipient id
|
||||
}
|
||||
|
||||
message StickerPack {
|
||||
bytes packId = 1;
|
||||
bytes packKey = 2;
|
||||
|
||||
@@ -15,7 +15,7 @@ androidx-window = "1.3.0"
|
||||
glide = "4.15.1"
|
||||
gradle = "8.9.0"
|
||||
kotlin = "2.2.20"
|
||||
libsignal-client = "0.86.6"
|
||||
libsignal-client = "0.86.8"
|
||||
mp4parser = "1.9.39"
|
||||
android-gradle-plugin = "8.10.1"
|
||||
accompanist = "0.28.0"
|
||||
|
||||
@@ -15400,28 +15400,20 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="57b3cf8f247f1990211110734a7d1af413db145c8f17eb1b2cdc9b9321188c2b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.signal" name="libsignal-android" version="0.86.6">
|
||||
<artifact name="libsignal-android-0.86.6.aar">
|
||||
<md5 value="2eea67b3317b3c6dcd470dc5cbf35523" origin="Generated by Gradle"/>
|
||||
<sha1 value="0377f9b317e71f380ec5678495388fbdd06d54c2" origin="Generated by Gradle"/>
|
||||
<sha256 value="4528ea6373cf15dba008effa33e24ee1006e358ab83e8fc2678734e44e6987db" origin="Generated by Gradle"/>
|
||||
<component group="org.signal" name="libsignal-android" version="0.86.8">
|
||||
<artifact name="libsignal-android-0.86.8.aar">
|
||||
<sha256 value="a9fc5fd6bf12fea69318f6c992f8f52804950bc7888ae3df3d7bad6b1c18e8a0" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="libsignal-android-0.86.6.module">
|
||||
<md5 value="45ce4a01f32a0e164996aa94a0d32d92" origin="Generated by Gradle"/>
|
||||
<sha1 value="9da5a2d8d3db6c5a3d61af6a73529ba79d9fc608" origin="Generated by Gradle"/>
|
||||
<sha256 value="944c7b8eda64a98c418713ed846ea539c6510e689239627ab4e7e75bceb6da7e" origin="Generated by Gradle"/>
|
||||
<artifact name="libsignal-android-0.86.8.module">
|
||||
<sha256 value="4ce19af1fc9442c8eb231eb1f2523557291fc9e9dce2520b45d4ab7c6e9642d7" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.signal" name="libsignal-client" version="0.86.6">
|
||||
<artifact name="libsignal-client-0.86.6.jar">
|
||||
<md5 value="1322e053a71d63ea708ef837c1eedf68" origin="Generated by Gradle"/>
|
||||
<sha1 value="39c8ca5e7d6e7a8d0e20107b9dcfb3da3b55fcef" origin="Generated by Gradle"/>
|
||||
<sha256 value="2124c5215f71209ec6ac3508f43ef6f3af74718911baa1d6185b9930d21ed5b7" origin="Generated by Gradle"/>
|
||||
<component group="org.signal" name="libsignal-client" version="0.86.8">
|
||||
<artifact name="libsignal-client-0.86.8.jar">
|
||||
<sha256 value="bac0edb5e25a73686aa418a6e43e0e81df1744e2751957a5dcb960e03d59a1c3" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="libsignal-client-0.86.6.module">
|
||||
<md5 value="1efa21eae93805299d1c29f07997109b" origin="Generated by Gradle"/>
|
||||
<sha1 value="acef64b2c3b02a0aa264853301fd86e9bd30b58d" origin="Generated by Gradle"/>
|
||||
<sha256 value="03cc60c8bbf9b114bfed8bf97aba68158d0e0ffc3cba8ec6f0af7c8c1e151bb1" origin="Generated by Gradle"/>
|
||||
<artifact name="libsignal-client-0.86.8.module">
|
||||
<sha256 value="b9e8afcc22d50a4818f9d56cb40beda66c6532710c432a27be6b05c4ba5301af" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.signal" name="ringrtc-android" version="2.60.5">
|
||||
|
||||
Reference in New Issue
Block a user