Update to latest Backup.proto.

This commit is contained in:
Greyson Parrelli 2024-07-29 10:26:19 -04:00 committed by mtang-signal
parent c2bdac80dc
commit fb2a332513
17 changed files with 211 additions and 227 deletions

View file

@ -128,7 +128,7 @@ class ImportExportTest {
linkPreviews = true,
notDiscoverableByPhoneNumber = true,
preferContactAvatars = true,
universalExpireTimer = 42,
universalExpireTimerSeconds = 42,
displayBadgesOnProfile = true,
keepMutedChatsArchived = true,
hasSetMyStoriesPrivacy = true,
@ -556,46 +556,6 @@ class ImportExportTest {
)
}
@Test
fun deletedDistributionList() {
val alexa = Recipient(
id = 4,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01",
e164 = 141255501234,
blocked = true,
visibility = Contact.Visibility.HIDDEN,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
profileFamilyName = "Kim",
hideStory = true
)
)
val importData = exportFrames(
*standardFrames,
alexa,
Recipient(
id = 6,
distributionList = DistributionListItem(
distributionId = DistributionId.create().asUuid().toByteArray().toByteString(),
deletionTimestamp = 12345L
)
)
)
import(importData)
val exported = BackupRepository.debugExport()
val expected = exportFrames(
*standardFrames,
alexa
)
compare(expected, exported)
}
@Test
fun chatThreads() {
importExport(
@ -1254,12 +1214,7 @@ class ImportExportTest {
chatId = 1,
authorId = alice.id,
dateSent = dateSentStart++,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = dateSentStart,
dateServerSent = dateSentStart,
read = true,
sealedSender = true
),
directionless = ChatItem.DirectionlessMessageDetails(),
updateMessage = ChatUpdateMessage(
simpleUpdate = SimpleChatUpdate(
type = SimpleChatUpdate.Type.fromValue(i)!!
@ -1287,12 +1242,7 @@ class ImportExportTest {
chatId = 1,
authorId = alice.id,
dateSent = dateSentStart++,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = dateSentStart,
dateServerSent = dateSentStart,
read = true,
sealedSender = true
),
directionless = ChatItem.DirectionlessMessageDetails(),
updateMessage = ChatUpdateMessage(
expirationTimerChange = ExpirationTimerChatUpdate(
1000
@ -1303,11 +1253,7 @@ class ImportExportTest {
chatId = 1,
authorId = selfRecipient.id,
dateSent = dateSentStart++,
outgoing = ChatItem.OutgoingMessageDetails(
sendStatus = listOf(
SendStatus(alice.id, deliveryStatus = SendStatus.Status.READ, sealedSender = true, lastStatusUpdateTimestamp = -1)
)
),
directionless = ChatItem.DirectionlessMessageDetails(),
updateMessage = ChatUpdateMessage(
expirationTimerChange = ExpirationTimerChatUpdate(
0
@ -1318,9 +1264,7 @@ class ImportExportTest {
chatId = 1,
authorId = selfRecipient.id,
dateSent = dateSentStart++,
outgoing = ChatItem.OutgoingMessageDetails(
sendStatus = listOf(SendStatus(alice.id, deliveryStatus = SendStatus.Status.READ, sealedSender = true, lastStatusUpdateTimestamp = -1))
),
directionless = ChatItem.DirectionlessMessageDetails(),
updateMessage = ChatUpdateMessage(
expirationTimerChange = ExpirationTimerChatUpdate(
10000
@ -1331,12 +1275,7 @@ class ImportExportTest {
chatId = 1,
authorId = alice.id,
dateSent = dateSentStart++,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = dateSentStart,
dateServerSent = dateSentStart,
read = true,
sealedSender = true
),
directionless = ChatItem.DirectionlessMessageDetails(),
updateMessage = ChatUpdateMessage(
expirationTimerChange = ExpirationTimerChatUpdate(
0
@ -1348,7 +1287,6 @@ class ImportExportTest {
@Test
fun profileChangeChatUpdateMessage() {
var dateSentStart = 100L
importExport(
*standardFrames,
alice,
@ -1356,13 +1294,8 @@ class ImportExportTest {
ChatItem(
chatId = 1,
authorId = alice.id,
dateSent = dateSentStart++,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = dateSentStart,
dateServerSent = dateSentStart,
read = true,
sealedSender = true
),
dateSent = 100L,
directionless = ChatItem.DirectionlessMessageDetails(),
updateMessage = ChatUpdateMessage(
profileChange = ProfileChangeChatUpdate(
previousName = "Aliceee Kim",
@ -1375,7 +1308,6 @@ class ImportExportTest {
@Test
fun threadMergeChatUpdate() {
var dateSentStart = 100L
importExport(
*standardFrames,
alice,
@ -1383,13 +1315,8 @@ class ImportExportTest {
ChatItem(
chatId = 1,
authorId = alice.id,
dateSent = dateSentStart++,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = dateSentStart,
dateServerSent = dateSentStart,
read = true,
sealedSender = true
),
dateSent = 100L,
directionless = ChatItem.DirectionlessMessageDetails(),
updateMessage = ChatUpdateMessage(
threadMerge = ThreadMergeChatUpdate(
previousE164 = 141255501237
@ -1409,13 +1336,8 @@ class ImportExportTest {
ChatItem(
chatId = 1,
authorId = alice.id,
dateSent = dateSentStart++,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = dateSentStart,
dateServerSent = dateSentStart,
read = true,
sealedSender = true
),
dateSent = dateSentStart,
directionless = ChatItem.DirectionlessMessageDetails(),
updateMessage = ChatUpdateMessage(
sessionSwitchover = SessionSwitchoverChatUpdate(
e164 = 141255501237

View file

@ -220,8 +220,8 @@ object BackupRepository {
backupTimeMs = exportState.backupTime
)
)
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
// writes from other threads are blocked. This is something to think more about.
// We're using a snapshot, so the transaction is more for perf than correctness
dbSnapshot.rawWritableDatabase.withinTransaction {
AccountDataProcessor.export(dbSnapshot, signalStoreSnapshot) {
writer.write(it)
@ -316,29 +316,29 @@ object BackupRepository {
SignalDatabase.recipients.setProfileSharing(selfId, true)
eventTimer.emit("setup")
val backupState = BackupState(backupKey)
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(backupState)
val importState = ImportState(backupKey)
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(importState)
val totalLength = frameReader.getStreamLength()
for (frame in frameReader) {
when {
frame.account != null -> {
AccountDataProcessor.import(frame.account, selfId)
AccountDataProcessor.import(frame.account, selfId, importState)
eventTimer.emit("account")
}
frame.recipient != null -> {
RecipientBackupProcessor.import(frame.recipient, backupState)
RecipientBackupProcessor.import(frame.recipient, importState)
eventTimer.emit("recipient")
}
frame.chat != null -> {
ChatBackupProcessor.import(frame.chat, backupState)
ChatBackupProcessor.import(frame.chat, importState)
eventTimer.emit("chat")
}
frame.adHocCall != null -> {
AdHocCallBackupProcessor.import(frame.adHocCall, backupState)
AdHocCallBackupProcessor.import(frame.adHocCall, importState)
eventTimer.emit("call")
}
@ -362,7 +362,7 @@ object BackupRepository {
eventTimer.emit("chatItem")
}
backupState.chatIdToLocalThreadId.values.forEach {
importState.chatIdToLocalThreadId.values.forEach {
SignalDatabase.threads.update(it, unarchive = false, allowDeletion = false)
}
}
@ -947,15 +947,17 @@ data class ArchivedMediaObject(val mediaId: String, val cdn: Int)
data class BackupDirectories(val backupDir: String, val mediaDir: String)
class ExportState(val backupTime: Long, val allowMediaBackup: Boolean) {
val recipientIds = HashSet<Long>()
val threadIds = HashSet<Long>()
val recipientIds: MutableSet<Long> = hashSetOf()
val threadIds: MutableSet<Long> = hashSetOf()
val localToRemoteCustomChatColors: MutableMap<Long, Int> = hashMapOf()
}
class BackupState(val backupKey: BackupKey) {
val backupToLocalRecipientId = HashMap<Long, RecipientId>()
val chatIdToLocalThreadId = HashMap<Long, Long>()
val chatIdToLocalRecipientId = HashMap<Long, RecipientId>()
val chatIdToBackupRecipientId = HashMap<Long, Long>()
class ImportState(val backupKey: BackupKey) {
val remoteToLocalRecipientId: MutableMap<Long, RecipientId> = hashMapOf()
val chatIdToLocalThreadId: MutableMap<Long, Long> = hashMapOf()
val chatIdToLocalRecipientId: MutableMap<Long, RecipientId> = hashMapOf()
val chatIdToBackupRecipientId: MutableMap<Long, Long> = hashMapOf()
val remoteToLocalColorId: MutableMap<Long, Long> = hashMapOf()
}
class BackupMetadata(

View file

@ -10,7 +10,7 @@ import android.database.sqlite.SQLiteDatabase
import androidx.core.content.contentValuesOf
import org.signal.core.util.requireLong
import org.signal.core.util.select
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.RecipientTable
@ -26,7 +26,7 @@ fun CallTable.getAdhocCallsForBackup(): CallLogIterator {
)
}
fun CallTable.restoreCallLogFromBackup(call: AdHocCall, backupState: BackupState) {
fun CallTable.restoreCallLogFromBackup(call: AdHocCall, importState: ImportState) {
val event = when (call.state) {
AdHocCall.State.GENERIC -> CallTable.Event.GENERIC_GROUP_CALL
AdHocCall.State.UNKNOWN_STATE -> CallTable.Event.GENERIC_GROUP_CALL
@ -34,7 +34,7 @@ fun CallTable.restoreCallLogFromBackup(call: AdHocCall, backupState: BackupState
val values = contentValuesOf(
CallTable.CALL_ID to call.callId,
CallTable.PEER to backupState.backupToLocalRecipientId[call.recipientId]!!.serialize(),
CallTable.PEER to importState.remoteToLocalRecipientId[call.recipientId]!!.serialize(),
CallTable.TYPE to CallTable.Type.serialize(CallTable.Type.AD_HOC_CALL),
CallTable.DIRECTION to CallTable.Direction.serialize(CallTable.Direction.OUTGOING),
CallTable.EVENT to CallTable.Event.serialize(event),

View file

@ -183,7 +183,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.REPORTED_SPAM)
}
MessageTypes.isExpirationTimerUpdate(record.type) -> {
builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate(record.expiresIn.toInt()))
builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate(record.expiresIn))
builder.expiresInMs = 0
}
MessageTypes.isProfileChange(record.type) -> {
@ -774,7 +774,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
mediaName = archiveMediaName ?: this.getMediaName().toString(),
cdnNumber = if (archiveMediaName != null) archiveCdn else Cdn.CDN_3.cdnNumber, // TODO (clark): Update when new proto with optional cdn is landed
key = Base64.decode(remoteKey).toByteString(),
size = this.size.toInt(),
size = this.size,
digest = remoteDigest.toByteString()
)
} else {

View file

@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.TombstoneAttachment
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
@ -89,7 +89,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
*/
class ChatItemImportInserter(
private val db: SQLiteDatabase,
private val backupState: BackupState,
private val importState: ImportState,
private val batchSize: Int
) {
companion object {
@ -155,25 +155,25 @@ class ChatItemImportInserter(
* If this item causes the buffer to hit the batch size, then a batch of items will actually be inserted.
*/
fun insert(chatItem: ChatItem) {
val fromLocalRecipientId: RecipientId? = backupState.backupToLocalRecipientId[chatItem.authorId]
val fromLocalRecipientId: RecipientId? = importState.remoteToLocalRecipientId[chatItem.authorId]
if (fromLocalRecipientId == null) {
Log.w(TAG, "[insert] Could not find a local recipient for backup recipient ID ${chatItem.authorId}! Skipping.")
return
}
val chatLocalRecipientId: RecipientId? = backupState.chatIdToLocalRecipientId[chatItem.chatId]
val chatLocalRecipientId: RecipientId? = importState.chatIdToLocalRecipientId[chatItem.chatId]
if (chatLocalRecipientId == null) {
Log.w(TAG, "[insert] Could not find a local recipient for chatId ${chatItem.chatId}! Skipping.")
return
}
val localThreadId: Long? = backupState.chatIdToLocalThreadId[chatItem.chatId]
val localThreadId: Long? = importState.chatIdToLocalThreadId[chatItem.chatId]
if (localThreadId == null) {
Log.w(TAG, "[insert] Could not find a local threadId for backup chatId ${chatItem.chatId}! Skipping.")
return
}
val chatBackupRecipientId: Long? = backupState.chatIdToBackupRecipientId[chatItem.chatId]
val chatBackupRecipientId: Long? = importState.chatIdToBackupRecipientId[chatItem.chatId]
if (chatBackupRecipientId == null) {
Log.w(TAG, "[insert] Could not find a backup recipientId for backup chatId ${chatItem.chatId}! Skipping.")
return
@ -283,7 +283,7 @@ class ChatItemImportInserter(
CallTable.MESSAGE_ID to messageRowId,
CallTable.PEER to chatRecipientId.serialize(),
CallTable.TYPE to CallTable.Type.serialize(CallTable.Type.GROUP_CALL),
CallTable.DIRECTION to CallTable.Direction.serialize(if (backupState.backupToLocalRecipientId[updateMessage.groupCall.ringerRecipientId] == selfId) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING),
CallTable.DIRECTION to CallTable.Direction.serialize(if (importState.remoteToLocalRecipientId[updateMessage.groupCall.ringerRecipientId] == selfId) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING),
CallTable.EVENT to CallTable.Event.serialize(
when (updateMessage.groupCall.state) {
GroupCall.State.ACCEPTED -> CallTable.Event.ACCEPTED
@ -460,8 +460,8 @@ class ChatItemImportInserter(
contentValues.put(MessageTable.UNIDENTIFIED, this.outgoing.sendStatus.count { it.sealedSender })
contentValues.put(MessageTable.READ, 1)
contentValues.addNetworkFailures(this, backupState)
contentValues.addIdentityKeyMismatches(this, backupState)
contentValues.addNetworkFailures(this, importState)
contentValues.addIdentityKeyMismatches(this, importState)
} else {
contentValues.put(MessageTable.VIEWED_COLUMN, 0)
contentValues.put(MessageTable.HAS_READ_RECEIPT, 0)
@ -529,7 +529,7 @@ class ChatItemImportInserter(
return reactions
.mapNotNull {
val authorId: Long? = backupState.backupToLocalRecipientId[it.authorId]?.toLong()
val authorId: Long? = importState.remoteToLocalRecipientId[it.authorId]?.toLong()
if (authorId != null) {
contentValuesOf(
@ -557,7 +557,7 @@ class ChatItemImportInserter(
}
return this.outgoing.sendStatus.mapNotNull { sendStatus ->
val recipientId = backupState.backupToLocalRecipientId[sendStatus.recipientId]
val recipientId = importState.remoteToLocalRecipientId[sendStatus.recipientId]
if (recipientId != null) {
contentValuesOf(
@ -674,7 +674,7 @@ class ChatItemImportInserter(
}
updateMessage.groupCall != null -> {
val startedCallRecipientId = if (updateMessage.groupCall.startedCallRecipientId != null) {
backupState.backupToLocalRecipientId[updateMessage.groupCall.startedCallRecipientId]
importState.remoteToLocalRecipientId[updateMessage.groupCall.startedCallRecipientId]
} else {
null
}
@ -815,7 +815,7 @@ class ChatItemImportInserter(
private fun ContentValues.addQuote(quote: Quote) {
this.put(MessageTable.QUOTE_ID, quote.targetSentTimestamp ?: MessageTable.QUOTE_TARGET_MISSING_ID)
this.put(MessageTable.QUOTE_AUTHOR, backupState.backupToLocalRecipientId[quote.authorId]!!.serialize())
this.put(MessageTable.QUOTE_AUTHOR, importState.remoteToLocalRecipientId[quote.authorId]!!.serialize())
this.put(MessageTable.QUOTE_BODY, quote.text)
this.put(MessageTable.QUOTE_TYPE, quote.type.toLocalQuoteType())
this.put(MessageTable.QUOTE_BODY_RANGES, quote.bodyRanges.toLocalBodyRanges()?.encode())
@ -840,14 +840,14 @@ class ChatItemImportInserter(
}
}
private fun ContentValues.addNetworkFailures(chatItem: ChatItem, backupState: BackupState) {
private fun ContentValues.addNetworkFailures(chatItem: ChatItem, importState: ImportState) {
if (chatItem.outgoing == null) {
return
}
val networkFailures = chatItem.outgoing.sendStatus
.filter { status -> status.networkFailure }
.mapNotNull { status -> backupState.backupToLocalRecipientId[status.recipientId] }
.mapNotNull { status -> importState.remoteToLocalRecipientId[status.recipientId] }
.map { recipientId -> NetworkFailure(recipientId) }
.toSet()
@ -856,14 +856,14 @@ class ChatItemImportInserter(
}
}
private fun ContentValues.addIdentityKeyMismatches(chatItem: ChatItem, backupState: BackupState) {
private fun ContentValues.addIdentityKeyMismatches(chatItem: ChatItem, importState: ImportState) {
if (chatItem.outgoing == null) {
return
}
val mismatches = chatItem.outgoing.sendStatus
.filter { status -> status.identityKeyMismatch }
.mapNotNull { status -> backupState.backupToLocalRecipientId[status.recipientId] }
.mapNotNull { status -> importState.remoteToLocalRecipientId[status.recipientId] }
.map { recipientId -> IdentityKeyMismatch(recipientId, null) } // TODO We probably want the actual identity key in this status situation?
.toSet()
@ -965,8 +965,8 @@ class ChatItemImportInserter(
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(),
archiveMediaId = importState.backupKey.deriveMediaId(MediaName(backupLocator.mediaName)).encode(),
archiveThumbnailMediaId = importState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(backupLocator.mediaName)).encode(),
digest = backupLocator.digest.toByteArray(),
incrementalMac = incrementalMac?.toByteArray(),
incrementalMacChunkSize = incrementalMacChunkSize,

View file

@ -15,7 +15,7 @@ import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireObject
import org.signal.core.util.select
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.DistributionList
import org.thoughtcrime.securesms.backup.v2.proto.DistributionListItem
import org.thoughtcrime.securesms.database.DistributionListTables
@ -98,7 +98,7 @@ fun DistributionListTables.getMembersForBackup(id: DistributionListId): List<Rec
}
}
fun DistributionListTables.restoreFromBackup(dlistItem: DistributionListItem, backupState: BackupState): RecipientId? {
fun DistributionListTables.restoreFromBackup(dlistItem: DistributionListItem, importState: ImportState): RecipientId? {
if (dlistItem.deletionTimestamp != null && dlistItem.deletionTimestamp > 0) {
val dlistId = createList(
name = "",
@ -115,7 +115,7 @@ fun DistributionListTables.restoreFromBackup(dlistItem: DistributionListItem, ba
val dlist = dlistItem.distributionList ?: return null
val members: List<RecipientId> = dlist.memberRecipientIds
.mapNotNull { backupState.backupToLocalRecipientId[it] }
.mapNotNull { importState.remoteToLocalRecipientId[it] }
if (members.size != dlist.memberRecipientIds.size) {
Log.w(TAG, "Couldn't find some member recipients! Missing backup recipientIds: ${dlist.memberRecipientIds.toSet() - members.toSet()}")

View file

@ -8,7 +8,7 @@ package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.SqlUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.select
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import java.util.concurrent.TimeUnit
@ -69,8 +69,8 @@ fun MessageTable.getMessagesForBackup(backupTime: Long, archiveMedia: Boolean):
return ChatItemExportIterator(cursor, 100, archiveMedia)
}
fun MessageTable.createChatItemInserter(backupState: BackupState): ChatItemImportInserter {
return ChatItemImportInserter(writableDatabase, backupState, 100)
fun MessageTable.createChatItemInserter(importState: ImportState): ChatItemImportInserter {
return ChatItemImportInserter(writableDatabase, importState, 100)
}
fun MessageTable.clearAllDataForBackupRestore() {

View file

@ -16,12 +16,12 @@ import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
import org.thoughtcrime.securesms.recipients.RecipientId
@ -29,7 +29,7 @@ import java.io.Closeable
private val TAG = Log.tag(ThreadTable::class.java)
fun ThreadTable.getThreadsForBackup(): ChatIterator {
fun ThreadTable.getThreadsForBackup(): ChatExportIterator {
//language=sql
val query = """
SELECT
@ -49,7 +49,7 @@ fun ThreadTable.getThreadsForBackup(): ChatIterator {
"""
val cursor = readableDatabase.query(query)
return ChatIterator(cursor)
return ChatExportIterator(cursor)
}
fun ThreadTable.clearAllDataForBackupRestore() {
@ -58,15 +58,10 @@ fun ThreadTable.clearAllDataForBackupRestore() {
clearCache()
}
fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId): Long? {
val chatColor = chat.style?.parseChatColor()
val chatColorWithId = if (chatColor != null && chatColor.id is ChatColors.Id.NotSet) {
val savedColors = SignalDatabase.chatColors.getSavedChatColors()
val match = savedColors.find { it.matchesWithoutId(chatColor) }
match ?: SignalDatabase.chatColors.saveChatColors(chatColor)
} else {
chatColor
}
fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importState: ImportState): Long? {
val chatColor = chat.style?.remoteToLocalChatColors(importState)
// TODO [backup] Wallpaper
val threadId = writableDatabase
.insertInto(ThreadTable.TABLE_NAME)
@ -85,8 +80,8 @@ fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId): Long? {
RecipientTable.MENTION_SETTING to (if (chat.dontNotifyForMentionsIfMuted) RecipientTable.MentionSetting.DO_NOT_NOTIFY.id else RecipientTable.MentionSetting.ALWAYS_NOTIFY.id),
RecipientTable.MUTE_UNTIL to chat.muteUntilMs,
RecipientTable.MESSAGE_EXPIRATION_TIME to chat.expirationTimerMs,
RecipientTable.CHAT_COLORS to chatColorWithId?.serialize()?.encode(),
RecipientTable.CUSTOM_CHAT_COLORS_ID to (chatColorWithId?.id ?: ChatColors.Id.NotSet).longValue
RecipientTable.CHAT_COLORS to chatColor?.serialize()?.encode(),
RecipientTable.CUSTOM_CHAT_COLORS_ID to (chatColor?.id ?: ChatColors.Id.NotSet).longValue
),
"${RecipientTable.ID} = ?",
SqlUtil.buildArgs(recipientId.toLong())
@ -95,7 +90,7 @@ fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId): Long? {
return threadId
}
class ChatIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
class ChatExportIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
@ -106,31 +101,32 @@ class ChatIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
}
val serializedChatColors = cursor.requireBlob(RecipientTable.CHAT_COLORS)
val customChatColorsId = ChatColors.Id.forLongValue(cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID))
val chatColors: ChatColors? = if (serializedChatColors != null) {
val chatColorId = ChatColors.Id.forLongValue(cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID))
val chatColors: ChatColors? = serializedChatColors?.let { serialized ->
try {
ChatColors.forChatColor(customChatColorsId, ChatColor.ADAPTER.decode(serializedChatColors))
ChatColors.forChatColor(chatColorId, ChatColor.ADAPTER.decode(serialized))
} catch (e: InvalidProtocolBufferException) {
null
}
} else {
null
}
var chatStyleBuilder: ChatStyle.Builder? = null
if (chatColors != null) {
chatStyleBuilder = ChatStyle.Builder()
val presetBubbleColor = chatColors.tryToMapToBackupPreset()
if (presetBubbleColor != null) {
chatStyleBuilder.bubbleColorPreset = presetBubbleColor
} else if (chatColors.isGradient()) {
chatStyleBuilder.bubbleGradient = ChatStyle.Gradient(angle = chatColors.getDegrees().toInt(), colors = chatColors.getColors().toList())
} else if (customChatColorsId is ChatColors.Id.Auto) {
chatStyleBuilder.autoBubbleColor = ChatStyle.AutomaticBubbleColor()
} else {
chatStyleBuilder.bubbleSolidColor = chatColors.asSingleColor()
when (chatColorId) {
ChatColors.Id.NotSet -> {}
ChatColors.Id.Auto -> {
chatStyleBuilder.autoBubbleColor = ChatStyle.AutomaticBubbleColor()
}
ChatColors.Id.BuiltIn -> {
chatStyleBuilder.bubbleColorPreset = chatColors.localToRemoteChatColors()
}
is ChatColors.Id.Custom -> {
chatStyleBuilder.customColorId = chatColorId.longValue
}
}
}
// TODO [backup] wallpaper
return Chat(
id = cursor.requireLong(ThreadTable.ID),
@ -150,9 +146,9 @@ class ChatIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
}
}
private fun ChatStyle.parseChatColor(): ChatColors? {
if (bubbleColorPreset != null) {
return when (bubbleColorPreset) {
private fun ChatStyle.remoteToLocalChatColors(importState: ImportState): ChatColors? {
if (this.bubbleColorPreset != null) {
return when (this.bubbleColorPreset) {
ChatStyle.BubbleColorPreset.SOLID_CRIMSON -> ChatColorsPalette.Bubbles.CRIMSON
ChatStyle.BubbleColorPreset.SOLID_VERMILION -> ChatColorsPalette.Bubbles.VERMILION
ChatStyle.BubbleColorPreset.SOLID_BURLAP -> ChatColorsPalette.Bubbles.BURLAP
@ -177,27 +173,22 @@ private fun ChatStyle.parseChatColor(): ChatColors? {
ChatStyle.BubbleColorPreset.UNKNOWN_BUBBLE_COLOR_PRESET, ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE -> ChatColorsPalette.Bubbles.ULTRAMARINE
}
}
if (autoBubbleColor != null) {
if (this.autoBubbleColor != null) {
return ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto)
}
if (bubbleSolidColor != null) {
return ChatColors(id = ChatColors.Id.NotSet, singleColor = bubbleSolidColor, linearGradient = null)
}
if (bubbleGradient != null) {
return ChatColors(
id = ChatColors.Id.NotSet,
singleColor = null,
linearGradient = ChatColors.LinearGradient(
degrees = bubbleGradient.angle.toFloat(),
colors = bubbleGradient.colors.toIntArray(),
positions = floatArrayOf(0f, 1f)
)
)
if (this.customColorId != null) {
return importState.remoteToLocalColorId[this.customColorId]?.let { localId ->
val colorId = ChatColors.Id.forLongValue(localId)
ChatColorsPalette.Bubbles.default.withId(colorId)
}
}
return null
}
private fun ChatColors.tryToMapToBackupPreset(): ChatStyle.BubbleColorPreset? {
private fun ChatColors.localToRemoteChatColors(): ChatStyle.BubbleColorPreset? {
when (this) {
// Solids
ChatColorsPalette.Bubbles.CRIMSON -> return ChatStyle.BubbleColorPreset.SOLID_CRIMSON

View file

@ -7,12 +7,16 @@ package org.thoughtcrime.securesms.backup.v2.processor
import okio.ByteString.Companion.EMPTY
import okio.ByteString.Companion.toByteString
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.database.restoreSelfFromBackup
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
@ -32,6 +36,8 @@ import java.util.Currency
object AccountDataProcessor {
private val TAG = Log.tag(AccountDataProcessor::class)
fun export(db: SignalDatabase, signalStore: SignalStore, emitter: BackupFrameEmitter) {
val context = AppDependencies.application
@ -53,7 +59,7 @@ object AccountDataProcessor {
AccountData.UsernameLink(
entropy = signalStore.accountValues.usernameLink?.entropy?.toByteString() ?: EMPTY,
serverId = signalStore.accountValues.usernameLink?.serverId?.toByteArray()?.toByteString() ?: EMPTY,
color = signalStore.miscValues.usernameQrCodeColorScheme.toBackupUsernameColor() ?: AccountData.UsernameLink.Color.BLUE
color = signalStore.miscValues.usernameQrCodeColorScheme.toBackupUsernameColor()
)
} else {
null
@ -67,7 +73,7 @@ object AccountDataProcessor {
notDiscoverableByPhoneNumber = signalStore.phoneNumberPrivacyValues.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE,
phoneNumberSharingMode = signalStore.phoneNumberPrivacyValues.phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
preferContactAvatars = signalStore.settingsValues.isPreferSystemContactPhotos,
universalExpireTimer = signalStore.settingsValues.universalExpireTimer,
universalExpireTimerSeconds = signalStore.settingsValues.universalExpireTimer,
preferredReactionEmoji = signalStore.emojiValues.rawReactions,
storiesDisabled = signalStore.storyValues.isFeatureDisabled,
hasViewedOnboardingStory = signalStore.storyValues.userHasViewedOnboardingStory,
@ -75,7 +81,8 @@ object AccountDataProcessor {
keepMutedChatsArchived = signalStore.settingsValues.shouldKeepMutedChatsArchived(),
displayBadgesOnProfile = signalStore.inAppPaymentValues.getDisplayBadgesOnProfile(),
hasSeenGroupStoryEducationSheet = signalStore.storyValues.userHasSeenGroupStoryEducationSheet,
hasCompletedUsernameOnboarding = signalStore.uiHintValues.hasCompletedUsernameOnboarding()
hasCompletedUsernameOnboarding = signalStore.uiHintValues.hasCompletedUsernameOnboarding(),
customChatColors = db.chatColorsTable.getSavedChatColors().toRemoteChatColors()
),
donationSubscriberData = donationSubscriber?.toSubscriberData(signalStore.inAppPaymentValues.isDonationSubscriptionManuallyCancelled())
)
@ -83,7 +90,7 @@ object AccountDataProcessor {
)
}
fun import(accountData: AccountData, selfId: RecipientId) {
fun import(accountData: AccountData, selfId: RecipientId, importState: ImportState) {
SignalDatabase.recipients.restoreSelfFromBackup(accountData, selfId)
SignalStore.account.setRegistered(true)
@ -99,7 +106,7 @@ object AccountDataProcessor {
SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = if (settings.notDiscoverableByPhoneNumber) PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberDiscoverabilityMode.DISCOVERABLE
SignalStore.phoneNumberPrivacy.phoneNumberSharingMode = settings.phoneNumberSharingMode.toLocalPhoneNumberMode()
SignalStore.settings.isPreferSystemContactPhotos = settings.preferContactAvatars
SignalStore.settings.universalExpireTimer = settings.universalExpireTimer
SignalStore.settings.universalExpireTimer = settings.universalExpireTimerSeconds
SignalStore.emoji.reactions = settings.preferredReactionEmoji
SignalStore.inAppPayments.setDisplayBadgesOnProfile(settings.displayBadgesOnProfile)
SignalStore.settings.setKeepMutedChatsArchived(settings.keepMutedChatsArchived)
@ -109,6 +116,32 @@ object AccountDataProcessor {
SignalStore.story.userHasSeenGroupStoryEducationSheet = settings.hasSeenGroupStoryEducationSheet
SignalStore.story.viewedReceiptsEnabled = settings.storyViewReceiptsEnabled ?: settings.readReceipts
settings.customChatColors
.mapNotNull { chatColor ->
val id = ChatColors.Id.forLongValue(chatColor.id)
when {
chatColor.solid != null -> {
ChatColors.forColor(id, chatColor.solid)
}
chatColor.gradient != null -> {
ChatColors.forGradient(
id,
ChatColors.LinearGradient(
degrees = chatColor.gradient.angle.toFloat(),
colors = chatColor.gradient.colors.toIntArray(),
positions = chatColor.gradient.positions.toFloatArray()
)
)
}
else -> null
}
}
.forEach { chatColor ->
// We need to use the "NotSet" chatId so that this operation is treated as an insert rather than an update
val saved = SignalDatabase.chatColors.saveChatColors(chatColor.withId(ChatColors.Id.NotSet))
importState.remoteToLocalColorId[chatColor.id.longValue] = saved.id.longValue
}
if (accountData.donationSubscriberData != null) {
if (accountData.donationSubscriberData.subscriberId.size > 0) {
val remoteSubscriberId = SubscriberId.fromBytes(accountData.donationSubscriberData.subscriberId.toByteArray())
@ -204,4 +237,28 @@ object AccountDataProcessor {
val currencyCode = currency.currencyCode
return AccountData.SubscriberData(subscriberId = subscriberId, currencyCode = currencyCode, manuallyCancelled = manuallyCancelled)
}
private fun List<ChatColors>.toRemoteChatColors(): List<ChatStyle.CustomChatColor> {
return this
.mapNotNull { local ->
if (local.linearGradient != null) {
ChatStyle.CustomChatColor(
id = local.id.longValue,
gradient = ChatStyle.Gradient(
angle = local.linearGradient.degrees.toInt(),
colors = local.linearGradient.colors.toList(),
positions = local.linearGradient.positions.toList()
)
)
} else if (local.singleColor != null) {
ChatStyle.CustomChatColor(
id = local.id.longValue,
solid = local.singleColor
)
} else {
Log.w(TAG, "Invalid custom color (id = ${local.id}, no gradient or solid color!")
null
}
}
}
}

View file

@ -6,7 +6,7 @@
package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.database.getAdhocCallsForBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreCallLogFromBackup
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
@ -28,7 +28,7 @@ object AdHocCallBackupProcessor {
}
}
fun import(call: AdHocCall, backupState: BackupState) {
SignalDatabase.calls.restoreCallLogFromBackup(call, backupState)
fun import(call: AdHocCall, importState: ImportState) {
SignalDatabase.calls.restoreCallLogFromBackup(call, importState)
}
}

View file

@ -6,8 +6,8 @@
package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.database.getThreadsForBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreFromBackup
import org.thoughtcrime.securesms.backup.v2.proto.Chat
@ -32,19 +32,17 @@ object ChatBackupProcessor {
}
}
fun import(chat: Chat, backupState: BackupState) {
val recipientId: RecipientId? = backupState.backupToLocalRecipientId[chat.recipientId]
fun import(chat: Chat, importState: ImportState) {
val recipientId: RecipientId? = importState.remoteToLocalRecipientId[chat.recipientId]
if (recipientId == null) {
Log.w(TAG, "Missing recipient for chat ${chat.id}")
return
}
SignalDatabase.threads.restoreFromBackup(chat, recipientId)?.let { threadId ->
backupState.chatIdToLocalRecipientId[chat.id] = recipientId
backupState.chatIdToLocalThreadId[chat.id] = threadId
backupState.chatIdToBackupRecipientId[chat.id] = chat.recipientId
SignalDatabase.threads.restoreFromBackup(chat, recipientId, importState)?.let { threadId ->
importState.chatIdToLocalRecipientId[chat.id] = recipientId
importState.chatIdToLocalThreadId[chat.id] = threadId
importState.chatIdToBackupRecipientId[chat.id] = chat.recipientId
}
// TODO there's several fields in the chat that actually need to be restored on the recipient table
}
}

View file

@ -6,8 +6,8 @@
package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
import org.thoughtcrime.securesms.backup.v2.database.createChatItemInserter
import org.thoughtcrime.securesms.backup.v2.database.getMessagesForBackup
@ -31,7 +31,7 @@ object ChatItemBackupProcessor {
}
}
fun beginImport(backupState: BackupState): ChatItemImportInserter {
return SignalDatabase.messages.createChatItemInserter(backupState)
fun beginImport(importState: ImportState): ChatItemImportInserter {
return SignalDatabase.messages.createChatItemInserter(importState)
}
}

View file

@ -6,8 +6,8 @@
package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.database.BackupRecipient
import org.thoughtcrime.securesms.backup.v2.database.getAllForBackup
import org.thoughtcrime.securesms.backup.v2.database.getCallLinksForBackup
@ -28,11 +28,11 @@ object RecipientBackupProcessor {
val TAG = Log.tag(RecipientBackupProcessor::class.java)
fun export(db: SignalDatabase, signalStore: SignalStore, state: ExportState, emitter: BackupFrameEmitter) {
fun export(db: SignalDatabase, signalStore: SignalStore, exportState: ExportState, emitter: BackupFrameEmitter) {
val selfId = db.recipientTable.getByAci(signalStore.accountValues.aci!!).get().toLong()
val releaseChannelId = signalStore.releaseChannelValues.releaseChannelRecipientId
if (releaseChannelId != null) {
state.recipientIds.add(releaseChannelId.toLong())
exportState.recipientIds.add(releaseChannelId.toLong())
emitter.emit(
Frame(
recipient = BackupRecipient(
@ -46,7 +46,7 @@ object RecipientBackupProcessor {
db.recipientTable.getContactsForBackup(selfId).use { reader ->
for (backupRecipient in reader) {
if (backupRecipient != null) {
state.recipientIds.add(backupRecipient.id)
exportState.recipientIds.add(backupRecipient.id)
emitter.emit(Frame(recipient = backupRecipient))
}
}
@ -54,27 +54,27 @@ object RecipientBackupProcessor {
db.recipientTable.getGroupsForBackup().use { reader ->
for (backupRecipient in reader) {
state.recipientIds.add(backupRecipient.id)
exportState.recipientIds.add(backupRecipient.id)
emitter.emit(Frame(recipient = backupRecipient))
}
}
db.distributionListTables.getAllForBackup().forEach {
state.recipientIds.add(it.id)
exportState.recipientIds.add(it.id)
emitter.emit(Frame(recipient = it))
}
db.callLinkTable.getCallLinksForBackup().forEach {
state.recipientIds.add(it.id)
exportState.recipientIds.add(it.id)
emitter.emit(Frame(recipient = it))
}
}
fun import(recipient: BackupRecipient, backupState: BackupState) {
fun import(recipient: BackupRecipient, importState: ImportState) {
val newId = when {
recipient.contact != null -> SignalDatabase.recipients.restoreContactFromBackup(recipient.contact)
recipient.group != null -> SignalDatabase.recipients.restoreGroupFromBackup(recipient.group)
recipient.distributionList != null -> SignalDatabase.distributionLists.restoreFromBackup(recipient.distributionList, backupState)
recipient.distributionList != null -> SignalDatabase.distributionLists.restoreFromBackup(recipient.distributionList, importState)
recipient.self != null -> Recipient.self().id
recipient.releaseNotes != null -> SignalDatabase.recipients.restoreReleaseNotes()
recipient.callLink != null -> SignalDatabase.callLinks.restoreFromBackup(recipient.callLink)
@ -84,7 +84,7 @@ object RecipientBackupProcessor {
}
}
if (newId != null) {
backupState.backupToLocalRecipientId[recipient.id] = newId
importState.remoteToLocalRecipientId[recipient.id] = newId
}
}
}

View file

@ -28,8 +28,8 @@ import kotlin.math.min
@Parcelize
class ChatColors(
val id: Id,
private val linearGradient: LinearGradient?,
private val singleColor: Int?
val linearGradient: LinearGradient?,
val singleColor: Int?
) : Parcelable {
fun isGradient(): Boolean = linearGradient != null

View file

@ -384,7 +384,7 @@ object GroupsV2UpdateMessageConverter {
updates.add(
GroupChangeChatUpdate.Update(
groupExpirationTimerUpdate = GroupExpirationTimerUpdate(
expiresInMs = (change.newTimer!!.duration * 1000L).toUInt().toInt(),
expiresInMs = (change.newTimer!!.duration * 1000L).toUInt().toLong(),
updaterAci = if (editorUnknown) null else change.editorServiceIdBytes
)
)

View file

@ -234,7 +234,7 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeGroupExpirationTimerUpdate(@NonNull GroupExpirationTimerUpdate update, @NonNull List<UpdateDescription> updates) {
final int duration = Math.toIntExact(Integer.toUnsignedLong(update.expiresInMs) / 1000);
final int duration = Math.toIntExact(update.expiresInMs / 1000);
String time = ExpirationUtil.getExpirationDisplayValue(context, duration);
if (update.updaterAci == null) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time), R.drawable.ic_update_timer_16));

View file

@ -63,7 +63,7 @@ message AccountData {
bool linkPreviews = 4;
bool notDiscoverableByPhoneNumber = 5;
bool preferContactAvatars = 6;
uint32 universalExpireTimer = 7; // 0 means no universal expire timer.
uint32 universalExpireTimerSeconds = 7; // 0 means no universal expire timer.
repeated string preferredReactionEmoji = 8;
bool displayBadgesOnProfile = 9;
bool keepMutedChatsArchived = 10;
@ -75,6 +75,7 @@ message AccountData {
bool hasCompletedUsernameOnboarding = 16;
PhoneNumberSharingMode phoneNumberSharingMode = 17;
ChatStyle defaultChatStyle = 18;
repeated ChatStyle.CustomChatColor customChatColors = 19;
}
message SubscriberData {
@ -564,7 +565,7 @@ message FilePointer {
optional uint32 cdnNumber = 2;
bytes key = 3;
bytes digest = 4;
uint32 size = 5;
uint64 size = 5;
// Fallback in case backup tier upload failed.
optional string transitCdnKey = 6;
optional uint32 transitCdnNumber = 7;
@ -756,7 +757,7 @@ message SimpleChatUpdate {
// For 1:1 chat updates only.
// For group thread updates use GroupExpirationTimerUpdate.
message ExpirationTimerChatUpdate {
uint32 expiresInMs = 1; // 0 means the expiration timer was disabled
uint64 expiresInMs = 1; // 0 means the expiration timer was disabled
}
message ProfileChangeChatUpdate {
@ -1022,7 +1023,7 @@ message GroupV2MigrationDroppedMembersUpdate {
// For 1:1 timer updates, use ExpirationTimerChatUpdate.
message GroupExpirationTimerUpdate {
uint32 expiresInMs = 1; // 0 means the expiration timer was disabled
uint64 expiresInMs = 1; // 0 means the expiration timer was disabled
optional bytes updaterAci = 2;
}
@ -1034,10 +1035,19 @@ message StickerPack {
message ChatStyle {
message Gradient {
uint32 angle = 1; // degrees
repeated uint32 colors = 2;
repeated fixed32 colors = 2;
repeated float positions = 3; // percent from 0 to 1
}
message CustomChatColor {
uint64 id = 1;
oneof color {
fixed32 solid = 2;
Gradient gradient = 3;
}
}
message AutomaticBubbleColor {
}
@ -1098,10 +1108,14 @@ message ChatStyle {
}
oneof bubbleColor {
BubbleColorPreset bubbleColorPreset = 3;
Gradient bubbleGradient = 4;
uint32 bubbleSolidColor = 5;
// Bubble setting is automatically determined based on the wallpaper setting.
AutomaticBubbleColor autoBubbleColor = 6;
// Bubble setting is automatically determined based on the wallpaper setting,
// or `SOLID_ULTRAMARINE` for `noWallpaper`
AutomaticBubbleColor autoBubbleColor = 3;
BubbleColorPreset bubbleColorPreset = 4;
// See AccountSettings.customChatColors
uint64 customColorId = 5;
}
bool dimWallpaperInDarkMode = 7;
}