Refactor backup exporting.

This commit is contained in:
Greyson Parrelli 2024-10-01 11:12:44 -04:00
parent 813a92380b
commit 13708e33e4
22 changed files with 1838 additions and 1735 deletions

View file

@ -0,0 +1,9 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
typealias ArchiveRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
typealias ArchiveGroup = org.thoughtcrime.securesms.backup.v2.proto.Group

View file

@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor
@ -285,7 +285,7 @@ object BackupRepository {
// We're using a snapshot, so the transaction is more for perf than correctness
dbSnapshot.rawWritableDatabase.withinTransaction {
progressEmitter?.onAccount()
AccountDataProcessor.export(dbSnapshot, signalStoreSnapshot) {
AccountDataBackupProcessor.export(dbSnapshot, signalStoreSnapshot) {
writer.write(it)
eventTimer.emit("account")
}
@ -412,7 +412,7 @@ object BackupRepository {
for (frame in frameReader) {
when {
frame.account != null -> {
AccountDataProcessor.import(frame.account, selfId, importState)
AccountDataBackupProcessor.import(frame.account, selfId, importState)
eventTimer.emit("account")
}

View file

@ -5,30 +5,25 @@
package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.select
import org.signal.ringrtc.CallLinkRootKey
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.backup.v2.proto.CallLink
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
import java.io.Closeable
import java.time.Instant
fun CallLinkTable.getCallLinksForBackup(): BackupCallLinkIterator {
fun CallLinkTable.getCallLinksForBackup(): CallLinkArchiveExportIterator {
val cursor = readableDatabase
.select()
.from(CallLinkTable.TABLE_NAME)
.run()
return BackupCallLinkIterator(cursor)
return CallLinkArchiveExportIterator(cursor)
}
fun CallLinkTable.restoreFromBackup(callLink: CallLink): RecipientId? {
@ -53,50 +48,6 @@ fun CallLinkTable.restoreFromBackup(callLink: CallLink): RecipientId? {
)
}
/**
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
*/
class BackupCallLinkIterator(private val cursor: Cursor) : Iterator<BackupRecipient>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): BackupRecipient {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val callLink = CallLinkTable.CallLinkDeserializer.deserialize(cursor)
return BackupRecipient(
id = callLink.recipientId.toLong(),
callLink = CallLink(
rootKey = callLink.credentials?.linkKeyBytes?.toByteString() ?: ByteString.EMPTY,
adminKey = callLink.credentials?.adminPassBytes?.toByteString(),
name = callLink.state.name,
expirationMs = try {
callLink.state.expiration.toEpochMilli()
} catch (e: ArithmeticException) {
Long.MAX_VALUE
},
restrictions = callLink.state.restrictions.toRemote()
)
)
}
override fun close() {
cursor.close()
}
}
private fun CallLinkState.Restrictions.toRemote(): CallLink.Restrictions {
return when (this) {
CallLinkState.Restrictions.ADMIN_APPROVAL -> CallLink.Restrictions.ADMIN_APPROVAL
CallLinkState.Restrictions.NONE -> CallLink.Restrictions.NONE
CallLinkState.Restrictions.UNKNOWN -> CallLink.Restrictions.UNKNOWN
}
}
private fun CallLink.Restrictions.toLocal(): CallLinkState.Restrictions {
return when (this) {
CallLink.Restrictions.ADMIN_APPROVAL -> CallLinkState.Restrictions.ADMIN_APPROVAL

View file

@ -5,18 +5,14 @@
package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import org.signal.core.util.insertInto
import org.signal.core.util.requireLong
import org.signal.core.util.select
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
import java.io.Closeable
fun CallTable.getAdhocCallsForBackup(): CallLogIterator {
return CallLogIterator(
fun CallTable.getAdhocCallsForBackup(): AdHocCallArchiveExportIterator {
return AdHocCallArchiveExportIterator(
readableDatabase
.select()
.from(CallTable.TABLE_NAME)
@ -31,7 +27,7 @@ fun CallTable.restoreCallLogFromBackup(call: AdHocCall, importState: ImportState
AdHocCall.State.UNKNOWN_STATE -> CallTable.Event.GENERIC_GROUP_CALL
}
val result = writableDatabase
writableDatabase
.insertInto(CallTable.TABLE_NAME)
.values(
CallTable.CALL_ID to call.callId,
@ -42,34 +38,4 @@ fun CallTable.restoreCallLogFromBackup(call: AdHocCall, importState: ImportState
CallTable.TIMESTAMP to call.callTimestamp
)
.run()
return Unit
}
/**
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
*/
class CallLogIterator(private val cursor: Cursor) : Iterator<AdHocCall?>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): AdHocCall? {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val callId = cursor.requireLong(CallTable.CALL_ID)
return AdHocCall(
callId = callId,
recipientId = cursor.requireLong(CallTable.PEER),
state = AdHocCall.State.GENERIC,
callTimestamp = cursor.requireLong(CallTable.TIMESTAMP)
)
}
override fun close() {
cursor.close()
}
}

View file

@ -5,81 +5,31 @@
package org.thoughtcrime.securesms.backup.v2.database
import okio.ByteString.Companion.toByteString
import org.signal.core.util.deleteAll
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireLong
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.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.DistributionList
import org.thoughtcrime.securesms.backup.v2.exporters.DistributionListArchiveExportIterator
import org.thoughtcrime.securesms.backup.v2.proto.DistributionListItem
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.api.util.toByteArray
import org.thoughtcrime.securesms.backup.v2.proto.DistributionList as BackupDistributionList
private val TAG = Log.tag(DistributionListTables::class.java)
data class DistributionRecipient(val id: RecipientId, val record: DistributionListRecord)
fun DistributionListTables.getAllForBackup(): List<BackupRecipient> {
val records = readableDatabase
fun DistributionListTables.getAllForBackup(): DistributionListArchiveExportIterator {
val cursor = readableDatabase
.select()
.from(DistributionListTables.ListTable.TABLE_NAME)
.run()
.readToList { cursor ->
val id: DistributionListId = DistributionListId.from(cursor.requireLong(DistributionListTables.ListTable.ID))
val privacyMode: DistributionListPrivacyMode = cursor.requireObject(DistributionListTables.ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
val recipientId: RecipientId = RecipientId.from(cursor.requireLong(DistributionListTables.ListTable.RECIPIENT_ID))
DistributionRecipient(
id = recipientId,
record = DistributionListRecord(
id = id,
name = cursor.requireNonNullString(DistributionListTables.ListTable.NAME),
distributionId = DistributionId.from(cursor.requireNonNullString(DistributionListTables.ListTable.DISTRIBUTION_ID)),
allowsReplies = cursor.requireBoolean(DistributionListTables.ListTable.ALLOWS_REPLIES),
rawMembers = getRawMembers(id, privacyMode),
members = getMembersForBackup(id),
deletedAtTimestamp = cursor.requireLong(DistributionListTables.ListTable.DELETION_TIMESTAMP),
isUnknown = cursor.requireBoolean(DistributionListTables.ListTable.IS_UNKNOWN),
privacyMode = privacyMode
)
)
}
return records
.map { recipient ->
BackupRecipient(
id = recipient.id.toLong(),
distributionList = if (recipient.record.deletedAtTimestamp != 0L) {
DistributionListItem(
distributionId = recipient.record.distributionId.asUuid().toByteArray().toByteString(),
deletionTimestamp = recipient.record.deletedAtTimestamp
)
} else {
DistributionListItem(
distributionId = recipient.record.distributionId.asUuid().toByteArray().toByteString(),
distributionList = DistributionList(
name = recipient.record.name,
allowReplies = recipient.record.allowsReplies,
privacyMode = recipient.record.privacyMode.toBackupPrivacyMode(),
memberRecipientIds = recipient.record.members.map { it.toLong() }
)
)
}
)
}
return DistributionListArchiveExportIterator(cursor, this)
}
fun DistributionListTables.getMembersForBackup(id: DistributionListId): List<RecipientId> {
@ -142,14 +92,6 @@ fun DistributionListTables.clearAllDataForBackupRestore() {
writableDatabase.deleteAll(DistributionListTables.MembershipTable.TABLE_NAME)
}
private fun DistributionListPrivacyMode.toBackupPrivacyMode(): BackupDistributionList.PrivacyMode {
return when (this) {
DistributionListPrivacyMode.ONLY_WITH -> BackupDistributionList.PrivacyMode.ONLY_WITH
DistributionListPrivacyMode.ALL -> BackupDistributionList.PrivacyMode.ALL
DistributionListPrivacyMode.ALL_EXCEPT -> BackupDistributionList.PrivacyMode.ALL_EXCEPT
}
}
private fun BackupDistributionList.PrivacyMode.toLocalPrivacyMode(): DistributionListPrivacyMode {
return when (this) {
BackupDistributionList.PrivacyMode.UNKNOWN -> DistributionListPrivacyMode.ALL

View file

@ -6,17 +6,17 @@
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.ImportState
import org.thoughtcrime.securesms.backup.v2.exporters.ChatItemArchiveExportIterator
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.SignalDatabase
import java.util.concurrent.TimeUnit
private val TAG = Log.tag(MessageTable::class.java)
private const val BASE_TYPE = "base_type"
private const val COLUMN_BASE_TYPE = "base_type"
fun MessageTable.getMessagesForBackup(backupTime: Long, mediaBackupEnabled: Boolean): ChatItemExportIterator {
fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, mediaBackupEnabled: Boolean): ChatItemArchiveExportIterator {
val cursor = readableDatabase
.select(
MessageTable.ID,
@ -50,7 +50,7 @@ fun MessageTable.getMessagesForBackup(backupTime: Long, mediaBackupEnabled: Bool
MessageTable.READ,
MessageTable.NETWORK_FAILURES,
MessageTable.MISMATCHED_IDENTITIES,
"${MessageTable.TYPE} & ${MessageTypes.BASE_TYPE_MASK} AS ${ChatItemExportIterator.COLUMN_BASE_TYPE}",
"${MessageTable.TYPE} & ${MessageTypes.BASE_TYPE_MASK} AS $COLUMN_BASE_TYPE",
MessageTable.MESSAGE_EXTRAS
)
.from(MessageTable.TABLE_NAME)
@ -66,7 +66,7 @@ fun MessageTable.getMessagesForBackup(backupTime: Long, mediaBackupEnabled: Bool
.orderBy("${MessageTable.DATE_RECEIVED} ASC")
.run()
return ChatItemExportIterator(cursor, 100, mediaBackupEnabled)
return ChatItemArchiveExportIterator(db, cursor, 100, mediaBackupEnabled)
}
fun MessageTable.createChatItemInserter(importState: ImportState): ChatItemImportInserter {

View file

@ -6,20 +6,12 @@
package org.thoughtcrime.securesms.backup.v2.database
import android.content.ContentValues
import android.database.Cursor
import androidx.core.content.contentValuesOf
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.deleteAll
import org.signal.core.util.logging.Log
import org.signal.core.util.nullIfBlank
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.requireNonNullBlob
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.toInt
import org.signal.core.util.update
@ -35,14 +27,14 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember
import org.signal.storageservice.protos.groups.local.DecryptedTimer
import org.signal.storageservice.protos.groups.local.EnabledState
import org.thoughtcrime.securesms.backup.v2.exporters.ContactArchiveExportIterator
import org.thoughtcrime.securesms.backup.v2.exporters.GroupArchiveExportIterator
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.Contact
import org.thoughtcrime.securesms.backup.v2.proto.Group
import org.thoughtcrime.securesms.backup.v2.proto.Self
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTableCursorUtil
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
import org.thoughtcrime.securesms.dependencies.AppDependencies
@ -58,18 +50,15 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.util.toByteArray
import java.io.Closeable
typealias BackupRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
typealias BackupGroup = Group
private typealias BackupRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
/**
* Fetches all individual contacts for backups and returns the result as an iterator.
* It's important to note that the iterator still needs to be closed after it's used.
* It's recommended to use `.use` or a try-with-resources pattern.
*/
fun RecipientTable.getContactsForBackup(selfId: Long): BackupContactIterator {
fun RecipientTable.getContactsForBackup(selfId: Long): ContactArchiveExportIterator {
val cursor = readableDatabase
.select(
RecipientTable.ID,
@ -104,10 +93,10 @@ fun RecipientTable.getContactsForBackup(selfId: Long): BackupContactIterator {
)
.run()
return BackupContactIterator(cursor, selfId)
return ContactArchiveExportIterator(cursor, selfId)
}
fun RecipientTable.getGroupsForBackup(): BackupGroupIterator {
fun RecipientTable.getGroupsForBackup(): GroupArchiveExportIterator {
val cursor = readableDatabase
.select(
"${RecipientTable.TABLE_NAME}.${RecipientTable.ID}",
@ -129,7 +118,7 @@ fun RecipientTable.getGroupsForBackup(): BackupGroupIterator {
.where("${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY} IS NOT NULL")
.run()
return BackupGroupIterator(cursor)
return GroupArchiveExportIterator(cursor)
}
/**
@ -226,7 +215,7 @@ fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId {
val decryptedState = if (group.snapshot == null) {
DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
} else {
group.snapshot.toDecryptedGroup(operations)
group.snapshot.toLocal(operations)
}
val values = ContentValues().apply {
@ -244,12 +233,30 @@ fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId {
val recipientId = writableDatabase.insert(RecipientTable.TABLE_NAME, null, values)
val restoredId = SignalDatabase.groups.create(masterKey, decryptedState, groupSendEndorsements = null)
if (restoredId != null) {
SignalDatabase.groups.setShowAsStoryState(restoredId, group.storySendMode.toGroupShowAsStoryState())
SignalDatabase.groups.setShowAsStoryState(restoredId, group.storySendMode.toLocal())
}
return RecipientId.from(recipientId)
}
private fun Group.StorySendMode.toLocal(): GroupTable.ShowAsStoryState {
return when (this) {
Group.StorySendMode.ENABLED -> GroupTable.ShowAsStoryState.ALWAYS
Group.StorySendMode.DISABLED -> GroupTable.ShowAsStoryState.NEVER
Group.StorySendMode.DEFAULT -> GroupTable.ShowAsStoryState.IF_ACTIVE
}
}
private fun Group.MemberPendingProfileKey.toLocal(operations: GroupsV2Operations.GroupOperations): DecryptedPendingMember {
return DecryptedPendingMember(
serviceIdBytes = member!!.userId,
role = member.role.toLocal(),
addedByAci = addedByUserId,
timestamp = timestamp,
serviceIdCipherText = operations.encryptServiceId(ServiceId.Companion.parseOrNull(member.userId))
)
}
private fun Contact.Visibility.toLocal(): Recipient.HiddenState {
return when (this) {
Contact.Visibility.VISIBLE -> Recipient.HiddenState.NOT_HIDDEN
@ -280,78 +287,10 @@ private fun Group.Member.Role.toLocal(): Member.Role {
}
}
private fun AccessControl.AccessRequired.toSnapshot(): Group.AccessControl.AccessRequired {
return when (this) {
AccessControl.AccessRequired.UNKNOWN -> Group.AccessControl.AccessRequired.UNKNOWN
AccessControl.AccessRequired.ANY -> Group.AccessControl.AccessRequired.ANY
AccessControl.AccessRequired.MEMBER -> Group.AccessControl.AccessRequired.MEMBER
AccessControl.AccessRequired.ADMINISTRATOR -> Group.AccessControl.AccessRequired.ADMINISTRATOR
AccessControl.AccessRequired.UNSATISFIABLE -> Group.AccessControl.AccessRequired.UNSATISFIABLE
}
}
private fun AccessControl.toSnapshot(): Group.AccessControl {
return Group.AccessControl(members = members.toSnapshot(), attributes = attributes.toSnapshot(), addFromInviteLink = addFromInviteLink.toSnapshot())
}
private fun Member.Role.toSnapshot(): Group.Member.Role {
return when (this) {
Member.Role.UNKNOWN -> Group.Member.Role.UNKNOWN
Member.Role.DEFAULT -> Group.Member.Role.DEFAULT
Member.Role.ADMINISTRATOR -> Group.Member.Role.ADMINISTRATOR
}
}
private fun DecryptedGroup.toSnapshot(): Group.GroupSnapshot? {
if (this.revision == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION || this.revision == GroupsV2StateProcessor.PLACEHOLDER_REVISION) {
return null
}
return Group.GroupSnapshot(
title = Group.GroupAttributeBlob(title = this.title),
avatarUrl = this.avatar,
disappearingMessagesTimer = this.disappearingMessagesTimer?.takeIf { it.duration > 0 }?.let { Group.GroupAttributeBlob(disappearingMessagesDuration = it.duration) },
accessControl = this.accessControl?.toSnapshot(),
version = this.revision,
members = this.members.map { it.toSnapshot() },
membersPendingProfileKey = this.pendingMembers.map { it.toSnapshot() },
membersPendingAdminApproval = this.requestingMembers.map { it.toSnapshot() },
inviteLinkPassword = this.inviteLinkPassword,
description = this.description.takeUnless { it.isBlank() }?.let { Group.GroupAttributeBlob(descriptionText = it) },
announcements_only = this.isAnnouncementGroup == EnabledState.ENABLED,
members_banned = this.bannedMembers.map { it.toSnapshot() }
)
}
private fun Group.Member.toLocal(): DecryptedMember {
return DecryptedMember(aciBytes = userId, role = role.toLocal(), joinedAtRevision = joinedAtVersion)
}
private fun DecryptedMember.toSnapshot(): Group.Member {
return Group.Member(userId = aciBytes, role = role.toSnapshot(), joinedAtVersion = joinedAtRevision)
}
private fun Group.MemberPendingProfileKey.toLocal(operations: GroupsV2Operations.GroupOperations): DecryptedPendingMember {
return DecryptedPendingMember(
serviceIdBytes = member!!.userId,
role = member.role.toLocal(),
addedByAci = addedByUserId,
timestamp = timestamp,
serviceIdCipherText = operations.encryptServiceId(ServiceId.Companion.parseOrNull(member.userId))
)
}
private fun DecryptedPendingMember.toSnapshot(): Group.MemberPendingProfileKey {
return Group.MemberPendingProfileKey(
member = Group.Member(
userId = this.serviceIdBytes,
role = this.role.toSnapshot()
),
addedByUserId = this.addedByAci,
timestamp = this.timestamp
)
}
private fun Group.MemberPendingAdminApproval.toLocal(): DecryptedRequestingMember {
return DecryptedRequestingMember(
aciBytes = this.userId,
@ -359,13 +298,6 @@ private fun Group.MemberPendingAdminApproval.toLocal(): DecryptedRequestingMembe
)
}
private fun DecryptedRequestingMember.toSnapshot(): Group.MemberPendingAdminApproval {
return Group.MemberPendingAdminApproval(
userId = this.aciBytes,
timestamp = this.timestamp
)
}
private fun Group.MemberBanned.toLocal(): DecryptedBannedMember {
return DecryptedBannedMember(
serviceIdBytes = this.userId,
@ -373,14 +305,7 @@ private fun Group.MemberBanned.toLocal(): DecryptedBannedMember {
)
}
private fun DecryptedBannedMember.toSnapshot(): Group.MemberBanned {
return Group.MemberBanned(
userId = this.serviceIdBytes,
timestamp = this.timestamp
)
}
private fun Group.GroupSnapshot.toDecryptedGroup(operations: GroupsV2Operations.GroupOperations): DecryptedGroup {
private fun Group.GroupSnapshot.toLocal(operations: GroupsV2Operations.GroupOperations): DecryptedGroup {
return DecryptedGroup(
title = this.title?.title ?: "",
avatar = this.avatarUrl,
@ -403,139 +328,6 @@ private fun Contact.toLocalExtras(): RecipientExtras {
)
}
/**
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
*/
class BackupContactIterator(private val cursor: Cursor, private val selfId: Long) : Iterator<BackupRecipient?>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): BackupRecipient? {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val id = cursor.requireLong(RecipientTable.ID)
if (id == selfId) {
return BackupRecipient(
id = id,
self = Self()
)
}
val aci = ACI.parseOrNull(cursor.requireString(RecipientTable.ACI_COLUMN))
val pni = PNI.parseOrNull(cursor.requireString(RecipientTable.PNI_COLUMN))
val e164 = cursor.requireString(RecipientTable.E164)?.e164ToLong()
val registeredState = RecipientTable.RegisteredState.fromId(cursor.requireInt(RecipientTable.REGISTERED))
val profileKey = cursor.requireString(RecipientTable.PROFILE_KEY)
val extras = RecipientTableCursorUtil.getExtras(cursor)
if (aci == null && pni == null && e164 == null) {
return null
}
val contactBuilder = Contact.Builder()
.aci(aci?.rawUuid?.toByteArray()?.toByteString())
.pni(pni?.rawUuid?.toByteArray()?.toByteString())
.username(cursor.requireString(RecipientTable.USERNAME))
.e164(cursor.requireString(RecipientTable.E164)?.e164ToLong())
.blocked(cursor.requireBoolean(RecipientTable.BLOCKED))
.visibility(Recipient.HiddenState.deserialize(cursor.requireInt(RecipientTable.HIDDEN)).toRemote())
.profileKey(if (profileKey != null) Base64.decode(profileKey).toByteString() else null)
.profileSharing(cursor.requireBoolean(RecipientTable.PROFILE_SHARING))
.profileGivenName(cursor.requireString(RecipientTable.PROFILE_GIVEN_NAME))
.profileFamilyName(cursor.requireString(RecipientTable.PROFILE_FAMILY_NAME))
.hideStory(extras?.hideStory() ?: false)
if (registeredState == RecipientTable.RegisteredState.REGISTERED) {
contactBuilder.registered = Contact.Registered()
} else {
contactBuilder.notRegistered = Contact.NotRegistered(unregisteredTimestamp = cursor.requireLong(RecipientTable.UNREGISTERED_TIMESTAMP))
}
return BackupRecipient(
id = id,
contact = contactBuilder.build()
)
}
override fun close() {
cursor.close()
}
}
private fun Recipient.HiddenState.toRemote(): Contact.Visibility {
return when (this) {
Recipient.HiddenState.NOT_HIDDEN -> return Contact.Visibility.VISIBLE
Recipient.HiddenState.HIDDEN -> return Contact.Visibility.HIDDEN
Recipient.HiddenState.HIDDEN_MESSAGE_REQUEST -> return Contact.Visibility.HIDDEN_MESSAGE_REQUEST
}
}
/**
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
*/
class BackupGroupIterator(private val cursor: Cursor) : Iterator<BackupRecipient>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): BackupRecipient {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val extras = RecipientTableCursorUtil.getExtras(cursor)
val showAsStoryState: GroupTable.ShowAsStoryState = GroupTable.ShowAsStoryState.deserialize(cursor.requireInt(GroupTable.SHOW_AS_STORY_STATE))
val decryptedGroup: DecryptedGroup = DecryptedGroup.ADAPTER.decode(cursor.requireBlob(GroupTable.V2_DECRYPTED_GROUP)!!)
return BackupRecipient(
id = cursor.requireLong(RecipientTable.ID),
group = BackupGroup(
masterKey = cursor.requireNonNullBlob(GroupTable.V2_MASTER_KEY).toByteString(),
whitelisted = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
hideStory = extras?.hideStory() ?: false,
storySendMode = showAsStoryState.toGroupStorySendMode(),
snapshot = decryptedGroup.toSnapshot()
)
)
}
override fun close() {
cursor.close()
}
}
private fun String.e164ToLong(): Long? {
val fixed = if (this.startsWith("+")) {
this.substring(1)
} else {
this
}
return fixed.toLongOrNull()
}
private fun GroupTable.ShowAsStoryState.toGroupStorySendMode(): Group.StorySendMode {
return when (this) {
GroupTable.ShowAsStoryState.ALWAYS -> Group.StorySendMode.ENABLED
GroupTable.ShowAsStoryState.NEVER -> Group.StorySendMode.DISABLED
GroupTable.ShowAsStoryState.IF_ACTIVE -> Group.StorySendMode.DEFAULT
}
}
private fun Group.StorySendMode.toGroupShowAsStoryState(): GroupTable.ShowAsStoryState {
return when (this) {
Group.StorySendMode.ENABLED -> GroupTable.ShowAsStoryState.ALWAYS
Group.StorySendMode.DISABLED -> GroupTable.ShowAsStoryState.NEVER
Group.StorySendMode.DEFAULT -> GroupTable.ShowAsStoryState.IF_ACTIVE
}
}
private val Contact.formattedE164: String?
get() {
return e164?.let {

View file

@ -5,21 +5,15 @@
package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import androidx.core.content.contentValuesOf
import org.signal.core.util.SqlUtil
import org.signal.core.util.decodeOrNull
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
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.toInt
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.exporters.ChatArchiveExportIterator
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.util.ChatStyleConverter
import org.thoughtcrime.securesms.backup.v2.util.parseChatWallpaper
import org.thoughtcrime.securesms.backup.v2.util.toLocal
import org.thoughtcrime.securesms.backup.v2.util.toLocalAttachment
@ -27,15 +21,12 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors
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.database.model.databaseprotos.Wallpaper
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.wallpaper.UriChatWallpaper
import java.io.Closeable
private val TAG = Log.tag(ThreadTable::class.java)
fun ThreadTable.getThreadsForBackup(db: SignalDatabase): ChatExportIterator {
fun ThreadTable.getThreadsForBackup(db: SignalDatabase): ChatArchiveExportIterator {
//language=sql
val query = """
SELECT
@ -57,7 +48,7 @@ fun ThreadTable.getThreadsForBackup(db: SignalDatabase): ChatExportIterator {
"""
val cursor = readableDatabase.query(query)
return ChatExportIterator(cursor, db)
return ChatArchiveExportIterator(cursor, db)
}
fun ThreadTable.clearAllDataForBackupRestore() {
@ -107,48 +98,3 @@ fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importSt
return threadId
}
class ChatExportIterator(private val cursor: Cursor, private val db: SignalDatabase) : Iterator<Chat>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): Chat {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val customChatColorsId = ChatColors.Id.forLongValue(cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID))
val chatColors: ChatColors? = cursor.requireBlob(RecipientTable.CHAT_COLORS)?.let { serializedChatColors ->
val chatColor = ChatColor.ADAPTER.decodeOrNull(serializedChatColors)
chatColor?.let { ChatColors.forChatColor(customChatColorsId, it) }
}
val chatWallpaper: Wallpaper? = cursor.requireBlob(RecipientTable.WALLPAPER)?.let { serializedWallpaper ->
Wallpaper.ADAPTER.decodeOrNull(serializedWallpaper)
}
return Chat(
id = cursor.requireLong(ThreadTable.ID),
recipientId = cursor.requireLong(ThreadTable.RECIPIENT_ID),
archived = cursor.requireBoolean(ThreadTable.ARCHIVED),
pinnedOrder = cursor.requireInt(ThreadTable.PINNED),
expirationTimerMs = cursor.requireLong(RecipientTable.MESSAGE_EXPIRATION_TIME),
expireTimerVersion = cursor.requireInt(RecipientTable.MESSAGE_EXPIRATION_TIME_VERSION),
muteUntilMs = cursor.requireLong(RecipientTable.MUTE_UNTIL),
markedUnread = ThreadTable.ReadStatus.deserialize(cursor.requireInt(ThreadTable.READ)) == ThreadTable.ReadStatus.FORCED_UNREAD,
dontNotifyForMentionsIfMuted = RecipientTable.MentionSetting.DO_NOT_NOTIFY.id == cursor.requireInt(RecipientTable.MENTION_SETTING),
style = ChatStyleConverter.constructRemoteChatStyle(
db = db,
chatColors = chatColors,
chatColorId = customChatColorsId,
chatWallpaper = chatWallpaper
)
)
}
override fun close() {
cursor.close()
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import org.signal.core.util.requireLong
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
import org.thoughtcrime.securesms.database.CallTable
import java.io.Closeable
/**
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
*/
class AdHocCallArchiveExportIterator(private val cursor: Cursor) : Iterator<AdHocCall>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): AdHocCall {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val callId = cursor.requireLong(CallTable.CALL_ID)
return AdHocCall(
callId = callId,
recipientId = cursor.requireLong(CallTable.PEER),
state = AdHocCall.State.GENERIC,
callTimestamp = cursor.requireLong(CallTable.TIMESTAMP)
)
}
override fun close() {
cursor.close()
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.backup.v2.ArchiveRecipient
import org.thoughtcrime.securesms.backup.v2.proto.CallLink
import org.thoughtcrime.securesms.database.CallLinkTable
import java.io.Closeable
/**
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
*/
class CallLinkArchiveExportIterator(private val cursor: Cursor) : Iterator<ArchiveRecipient>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): ArchiveRecipient {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val callLink = CallLinkTable.CallLinkDeserializer.deserialize(cursor)
return ArchiveRecipient(
id = callLink.recipientId.toLong(),
callLink = CallLink(
rootKey = callLink.credentials?.linkKeyBytes?.toByteString() ?: ByteString.EMPTY,
adminKey = callLink.credentials?.adminPassBytes?.toByteString(),
name = callLink.state.name,
expirationMs = try {
callLink.state.expiration.toEpochMilli()
} catch (e: ArithmeticException) {
Long.MAX_VALUE
},
restrictions = callLink.state.restrictions.toRemote()
)
)
}
override fun close() {
cursor.close()
}
}
private fun CallLinkState.Restrictions.toRemote(): CallLink.Restrictions {
return when (this) {
CallLinkState.Restrictions.ADMIN_APPROVAL -> CallLink.Restrictions.ADMIN_APPROVAL
CallLinkState.Restrictions.NONE -> CallLink.Restrictions.NONE
CallLinkState.Restrictions.UNKNOWN -> CallLink.Restrictions.UNKNOWN
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.exporters
import android.database.Cursor
import org.signal.core.util.decodeOrNull
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.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.util.ChatStyleConverter
import org.thoughtcrime.securesms.conversation.colors.ChatColors
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.database.model.databaseprotos.Wallpaper
import java.io.Closeable
class ChatArchiveExportIterator(private val cursor: Cursor, private val db: SignalDatabase) : Iterator<Chat>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): Chat {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val customChatColorsId = ChatColors.Id.forLongValue(cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID))
val chatColors: ChatColors? = cursor.requireBlob(RecipientTable.CHAT_COLORS)?.let { serializedChatColors ->
val chatColor = ChatColor.ADAPTER.decodeOrNull(serializedChatColors)
chatColor?.let { ChatColors.forChatColor(customChatColorsId, it) }
}
val chatWallpaper: Wallpaper? = cursor.requireBlob(RecipientTable.WALLPAPER)?.let { serializedWallpaper ->
Wallpaper.ADAPTER.decodeOrNull(serializedWallpaper)
}
return Chat(
id = cursor.requireLong(ThreadTable.ID),
recipientId = cursor.requireLong(ThreadTable.RECIPIENT_ID),
archived = cursor.requireBoolean(ThreadTable.ARCHIVED),
pinnedOrder = cursor.requireInt(ThreadTable.PINNED),
expirationTimerMs = cursor.requireLong(RecipientTable.MESSAGE_EXPIRATION_TIME),
expireTimerVersion = cursor.requireInt(RecipientTable.MESSAGE_EXPIRATION_TIME_VERSION),
muteUntilMs = cursor.requireLong(RecipientTable.MUTE_UNTIL),
markedUnread = ThreadTable.ReadStatus.deserialize(cursor.requireInt(ThreadTable.READ)) == ThreadTable.ReadStatus.FORCED_UNREAD,
dontNotifyForMentionsIfMuted = RecipientTable.MentionSetting.DO_NOT_NOTIFY.id == cursor.requireInt(RecipientTable.MENTION_SETTING),
style = ChatStyleConverter.constructRemoteChatStyle(
db = db,
chatColors = chatColors,
chatColorId = customChatColorsId,
chatWallpaper = chatWallpaper
)
)
}
override fun close() {
cursor.close()
}
}

View file

@ -0,0 +1,102 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.exporters
import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.thoughtcrime.securesms.backup.v2.ArchiveRecipient
import org.thoughtcrime.securesms.backup.v2.proto.Contact
import org.thoughtcrime.securesms.backup.v2.proto.Self
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTableCursorUtil
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.util.toByteArray
import java.io.Closeable
/**
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [ArchiveRecipient]s.
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
*/
class ContactArchiveExportIterator(private val cursor: Cursor, private val selfId: Long) : Iterator<ArchiveRecipient>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): ArchiveRecipient {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val id = cursor.requireLong(RecipientTable.ID)
if (id == selfId) {
return ArchiveRecipient(
id = id,
self = Self()
)
}
val aci = ServiceId.ACI.Companion.parseOrNull(cursor.requireString(RecipientTable.ACI_COLUMN))
val pni = ServiceId.PNI.Companion.parseOrNull(cursor.requireString(RecipientTable.PNI_COLUMN))
val e164 = cursor.requireString(RecipientTable.E164)?.e164ToLong()
if (aci == null && pni == null && e164 == null) {
throw IllegalStateException("Should not happen! Query guards against this.")
}
val contactBuilder = Contact.Builder()
.aci(aci?.rawUuid?.toByteArray()?.toByteString())
.pni(pni?.rawUuid?.toByteArray()?.toByteString())
.username(cursor.requireString(RecipientTable.USERNAME))
.e164(cursor.requireString(RecipientTable.E164)?.e164ToLong())
.blocked(cursor.requireBoolean(RecipientTable.BLOCKED))
.visibility(Recipient.HiddenState.deserialize(cursor.requireInt(RecipientTable.HIDDEN)).toRemote())
.profileKey(cursor.requireString(RecipientTable.PROFILE_KEY)?.let { Base64.decode(it) }?.toByteString())
.profileSharing(cursor.requireBoolean(RecipientTable.PROFILE_SHARING))
.profileGivenName(cursor.requireString(RecipientTable.PROFILE_GIVEN_NAME))
.profileFamilyName(cursor.requireString(RecipientTable.PROFILE_FAMILY_NAME))
.hideStory(RecipientTableCursorUtil.getExtras(cursor)?.hideStory() ?: false)
val registeredState = RecipientTable.RegisteredState.fromId(cursor.requireInt(RecipientTable.REGISTERED))
if (registeredState == RecipientTable.RegisteredState.REGISTERED) {
contactBuilder.registered = Contact.Registered()
} else {
contactBuilder.notRegistered = Contact.NotRegistered(unregisteredTimestamp = cursor.requireLong(RecipientTable.UNREGISTERED_TIMESTAMP))
}
return ArchiveRecipient(
id = id,
contact = contactBuilder.build()
)
}
override fun close() {
cursor.close()
}
}
private fun Recipient.HiddenState.toRemote(): Contact.Visibility {
return when (this) {
Recipient.HiddenState.NOT_HIDDEN -> return Contact.Visibility.VISIBLE
Recipient.HiddenState.HIDDEN -> return Contact.Visibility.HIDDEN
Recipient.HiddenState.HIDDEN_MESSAGE_REQUEST -> return Contact.Visibility.HIDDEN_MESSAGE_REQUEST
}
}
private fun String.e164ToLong(): Long? {
val fixed = if (this.startsWith("+")) {
this.substring(1)
} else {
this
}
return fixed.toLongOrNull()
}

View file

@ -0,0 +1,91 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.exporters
import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireObject
import org.thoughtcrime.securesms.backup.v2.ArchiveRecipient
import org.thoughtcrime.securesms.backup.v2.database.getMembersForBackup
import org.thoughtcrime.securesms.backup.v2.proto.DistributionList
import org.thoughtcrime.securesms.backup.v2.proto.DistributionListItem
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.util.toByteArray
import java.io.Closeable
class DistributionListArchiveExportIterator(
private val cursor: Cursor,
private val distributionListTables: DistributionListTables
) : Iterator<ArchiveRecipient>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): ArchiveRecipient {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val id: DistributionListId = DistributionListId.from(cursor.requireLong(DistributionListTables.ListTable.ID))
val privacyMode: DistributionListPrivacyMode = cursor.requireObject(DistributionListTables.ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
val recipientId: RecipientId = RecipientId.from(cursor.requireLong(DistributionListTables.ListTable.RECIPIENT_ID))
val record = DistributionListRecord(
id = id,
name = cursor.requireNonNullString(DistributionListTables.ListTable.NAME),
distributionId = DistributionId.from(cursor.requireNonNullString(DistributionListTables.ListTable.DISTRIBUTION_ID)),
allowsReplies = cursor.requireBoolean(DistributionListTables.ListTable.ALLOWS_REPLIES),
rawMembers = distributionListTables.getRawMembers(id, privacyMode),
members = distributionListTables.getMembersForBackup(id),
deletedAtTimestamp = cursor.requireLong(DistributionListTables.ListTable.DELETION_TIMESTAMP),
isUnknown = cursor.requireBoolean(DistributionListTables.ListTable.IS_UNKNOWN),
privacyMode = privacyMode
)
val distributionListItem = if (record.deletedAtTimestamp != 0L) {
DistributionListItem(
distributionId = record.distributionId.asUuid().toByteArray().toByteString(),
deletionTimestamp = record.deletedAtTimestamp
)
} else {
DistributionListItem(
distributionId = record.distributionId.asUuid().toByteArray().toByteString(),
distributionList = DistributionList(
name = record.name,
allowReplies = record.allowsReplies,
privacyMode = record.privacyMode.toBackupPrivacyMode(),
memberRecipientIds = record.members.map { it.toLong() }
)
)
}
return ArchiveRecipient(
id = recipientId.toLong(),
distributionList = distributionListItem
)
}
override fun close() {
cursor.close()
}
}
private fun DistributionListPrivacyMode.toBackupPrivacyMode(): DistributionList.PrivacyMode {
return when (this) {
DistributionListPrivacyMode.ONLY_WITH -> DistributionList.PrivacyMode.ONLY_WITH
DistributionListPrivacyMode.ALL -> DistributionList.PrivacyMode.ALL
DistributionListPrivacyMode.ALL_EXCEPT -> DistributionList.PrivacyMode.ALL_EXCEPT
}
}

View file

@ -0,0 +1,147 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.exporters
import android.database.Cursor
import okio.ByteString.Companion.toByteString
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.requireNonNullBlob
import org.signal.storageservice.protos.groups.AccessControl
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember
import org.signal.storageservice.protos.groups.local.EnabledState
import org.thoughtcrime.securesms.backup.v2.ArchiveGroup
import org.thoughtcrime.securesms.backup.v2.ArchiveRecipient
import org.thoughtcrime.securesms.backup.v2.proto.Group
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTableCursorUtil
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import java.io.Closeable
/**
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [ArchiveRecipient]s.
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
*/
class GroupArchiveExportIterator(private val cursor: Cursor) : Iterator<ArchiveRecipient>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): ArchiveRecipient {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val extras = RecipientTableCursorUtil.getExtras(cursor)
val showAsStoryState: GroupTable.ShowAsStoryState = GroupTable.ShowAsStoryState.deserialize(cursor.requireInt(GroupTable.SHOW_AS_STORY_STATE))
val decryptedGroup: DecryptedGroup = DecryptedGroup.ADAPTER.decode(cursor.requireBlob(GroupTable.V2_DECRYPTED_GROUP)!!)
return ArchiveRecipient(
id = cursor.requireLong(RecipientTable.ID),
group = ArchiveGroup(
masterKey = cursor.requireNonNullBlob(GroupTable.V2_MASTER_KEY).toByteString(),
whitelisted = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
hideStory = extras?.hideStory() ?: false,
storySendMode = showAsStoryState.toRemote(),
snapshot = decryptedGroup.toRemote()
)
)
}
override fun close() {
cursor.close()
}
}
private fun GroupTable.ShowAsStoryState.toRemote(): Group.StorySendMode {
return when (this) {
GroupTable.ShowAsStoryState.ALWAYS -> Group.StorySendMode.ENABLED
GroupTable.ShowAsStoryState.NEVER -> Group.StorySendMode.DISABLED
GroupTable.ShowAsStoryState.IF_ACTIVE -> Group.StorySendMode.DEFAULT
}
}
private fun DecryptedGroup.toRemote(): Group.GroupSnapshot? {
if (this.revision == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION || this.revision == GroupsV2StateProcessor.PLACEHOLDER_REVISION) {
return null
}
return Group.GroupSnapshot(
title = Group.GroupAttributeBlob(title = this.title),
avatarUrl = this.avatar,
disappearingMessagesTimer = this.disappearingMessagesTimer?.takeIf { it.duration > 0 }?.let { Group.GroupAttributeBlob(disappearingMessagesDuration = it.duration) },
accessControl = this.accessControl?.toRemote(),
version = this.revision,
members = this.members.map { it.toRemote() },
membersPendingProfileKey = this.pendingMembers.map { it.toRemote() },
membersPendingAdminApproval = this.requestingMembers.map { it.toRemote() },
inviteLinkPassword = this.inviteLinkPassword,
description = this.description.takeUnless { it.isBlank() }?.let { Group.GroupAttributeBlob(descriptionText = it) },
announcements_only = this.isAnnouncementGroup == EnabledState.ENABLED,
members_banned = this.bannedMembers.map { it.toRemote() }
)
}
private fun AccessControl.AccessRequired.toRemote(): Group.AccessControl.AccessRequired {
return when (this) {
AccessControl.AccessRequired.UNKNOWN -> Group.AccessControl.AccessRequired.UNKNOWN
AccessControl.AccessRequired.ANY -> Group.AccessControl.AccessRequired.ANY
AccessControl.AccessRequired.MEMBER -> Group.AccessControl.AccessRequired.MEMBER
AccessControl.AccessRequired.ADMINISTRATOR -> Group.AccessControl.AccessRequired.ADMINISTRATOR
AccessControl.AccessRequired.UNSATISFIABLE -> Group.AccessControl.AccessRequired.UNSATISFIABLE
}
}
private fun AccessControl.toRemote(): Group.AccessControl {
return Group.AccessControl(members = members.toRemote(), attributes = attributes.toRemote(), addFromInviteLink = addFromInviteLink.toRemote())
}
private fun Member.Role.toRemote(): Group.Member.Role {
return when (this) {
Member.Role.UNKNOWN -> Group.Member.Role.UNKNOWN
Member.Role.DEFAULT -> Group.Member.Role.DEFAULT
Member.Role.ADMINISTRATOR -> Group.Member.Role.ADMINISTRATOR
}
}
private fun DecryptedMember.toRemote(): Group.Member {
return Group.Member(userId = aciBytes, role = role.toRemote(), joinedAtVersion = joinedAtRevision)
}
private fun DecryptedPendingMember.toRemote(): Group.MemberPendingProfileKey {
return Group.MemberPendingProfileKey(
member = Group.Member(
userId = this.serviceIdBytes,
role = this.role.toRemote()
),
addedByUserId = this.addedByAci,
timestamp = this.timestamp
)
}
private fun DecryptedBannedMember.toRemote(): Group.MemberBanned {
return Group.MemberBanned(
userId = this.serviceIdBytes,
timestamp = this.timestamp
)
}
private fun DecryptedRequestingMember.toRemote(): Group.MemberPendingAdminApproval {
return Group.MemberPendingAdminApproval(
userId = this.aciBytes,
timestamp = this.timestamp
)
}

View file

@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.backup.v2.processor
import android.content.Context
import okio.ByteString.Companion.EMPTY
import okio.ByteString.Companion.toByteString
import org.signal.core.util.logging.Log
@ -40,9 +41,12 @@ import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.api.util.toByteArray
import java.util.Currency
object AccountDataProcessor {
/**
* Handles importing/exporting [AccountData] frames for an archive.
*/
object AccountDataBackupProcessor {
private val TAG = Log.tag(AccountDataProcessor::class)
private val TAG = Log.tag(AccountDataBackupProcessor::class)
fun export(db: SignalDatabase, signalStore: SignalStore, emitter: BackupFrameEmitter) {
val context = AppDependencies.application
@ -68,7 +72,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()
color = signalStore.miscValues.usernameQrCodeColorScheme.toRemoteUsernameColor()
)
} else {
null
@ -80,7 +84,7 @@ object AccountDataProcessor {
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
linkPreviews = signalStore.settingsValues.isLinkPreviewsEnabled,
notDiscoverableByPhoneNumber = signalStore.phoneNumberPrivacyValues.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE,
phoneNumberSharingMode = signalStore.phoneNumberPrivacyValues.phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
phoneNumberSharingMode = signalStore.phoneNumberPrivacyValues.phoneNumberSharingMode.toRemotePhoneNumberSharingMode(),
preferContactAvatars = signalStore.settingsValues.isPreferSystemContactPhotos,
universalExpireTimerSeconds = signalStore.settingsValues.universalExpireTimer,
preferredReactionEmoji = signalStore.emojiValues.rawReactions,
@ -114,107 +118,42 @@ object AccountDataProcessor {
val settings = accountData.accountSettings
if (settings != null) {
TextSecurePreferences.setReadReceiptsEnabled(context, settings.readReceipts)
TextSecurePreferences.setTypingIndicatorsEnabled(context, settings.typingIndicators)
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, settings.sealedSenderIndicators)
SignalStore.settings.isLinkPreviewsEnabled = settings.linkPreviews
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.universalExpireTimerSeconds
SignalStore.emoji.reactions = settings.preferredReactionEmoji
SignalStore.inAppPayments.setDisplayBadgesOnProfile(settings.displayBadgesOnProfile)
SignalStore.settings.setKeepMutedChatsArchived(settings.keepMutedChatsArchived)
SignalStore.story.userHasBeenNotifiedAboutStories = settings.hasSetMyStoriesPrivacy
SignalStore.story.userHasViewedOnboardingStory = settings.hasViewedOnboardingStory
SignalStore.story.isFeatureDisabled = settings.storiesDisabled
SignalStore.story.userHasSeenGroupStoryEducationSheet = settings.hasSeenGroupStoryEducationSheet
SignalStore.story.viewedReceiptsEnabled = settings.storyViewReceiptsEnabled ?: settings.readReceipts
importSettings(context, settings, importState)
}
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())
val localSubscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
if (settings.defaultChatStyle != null) {
val chatColors = settings.defaultChatStyle.toLocal(importState)
SignalStore.chatColors.chatColors = chatColors
val wallpaperAttachmentId: AttachmentId? = settings.defaultChatStyle.wallpaperPhoto?.let { filePointer ->
filePointer.toLocalAttachment(importState)?.let {
SignalDatabase.attachments.restoreWallpaperAttachment(it)
}
}
SignalStore.wallpaper.wallpaper = settings.defaultChatStyle.parseChatWallpaper(wallpaperAttachmentId)
} else {
SignalStore.chatColors.chatColors = null
SignalStore.wallpaper.wallpaper = null
}
if (accountData.donationSubscriberData != null) {
if (accountData.donationSubscriberData.subscriberId.size > 0) {
val remoteSubscriberId = SubscriberId.fromBytes(accountData.donationSubscriberData.subscriberId.toByteArray())
val localSubscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
val subscriber = InAppPaymentSubscriberRecord(
remoteSubscriberId,
Currency.getInstance(accountData.donationSubscriberData.currencyCode),
InAppPaymentSubscriberRecord.Type.DONATION,
localSubscriber?.requiresCancel ?: accountData.donationSubscriberData.manuallyCancelled,
InAppPaymentsRepository.getLatestPaymentMethodType(InAppPaymentSubscriberRecord.Type.DONATION)
)
InAppPaymentsRepository.setSubscriber(subscriber)
}
if (accountData.donationSubscriberData.manuallyCancelled) {
SignalStore.inAppPayments.updateLocalStateForManualCancellation(InAppPaymentSubscriberRecord.Type.DONATION)
}
}
if (accountData.avatarUrlPath.isNotEmpty()) {
AppDependencies.jobManager.add(RetrieveProfileAvatarJob(Recipient.self().fresh(), accountData.avatarUrlPath))
}
if (accountData.usernameLink != null) {
SignalStore.account.usernameLink = UsernameLinkComponents(
accountData.usernameLink.entropy.toByteArray(),
UuidUtil.parseOrThrow(accountData.usernameLink.serverId.toByteArray())
val subscriber = InAppPaymentSubscriberRecord(
remoteSubscriberId,
Currency.getInstance(accountData.donationSubscriberData.currencyCode),
InAppPaymentSubscriberRecord.Type.DONATION,
localSubscriber?.requiresCancel ?: accountData.donationSubscriberData.manuallyCancelled,
InAppPaymentsRepository.getLatestPaymentMethodType(InAppPaymentSubscriberRecord.Type.DONATION)
)
SignalStore.misc.usernameQrCodeColorScheme = accountData.usernameLink.color.toLocalUsernameColor()
} else {
SignalStore.account.usernameLink = null
InAppPaymentsRepository.setSubscriber(subscriber)
}
if (settings.preferredReactionEmoji.isNotEmpty()) {
SignalStore.emoji.reactions = settings.preferredReactionEmoji
if (accountData.donationSubscriberData.manuallyCancelled) {
SignalStore.inAppPayments.updateLocalStateForManualCancellation(InAppPaymentSubscriberRecord.Type.DONATION)
}
}
if (settings.hasCompletedUsernameOnboarding) {
SignalStore.uiHints.setHasCompletedUsernameOnboarding(true)
}
if (accountData.avatarUrlPath.isNotEmpty()) {
AppDependencies.jobManager.add(RetrieveProfileAvatarJob(Recipient.self().fresh(), accountData.avatarUrlPath))
}
if (accountData.usernameLink != null) {
SignalStore.account.usernameLink = UsernameLinkComponents(
accountData.usernameLink.entropy.toByteArray(),
UuidUtil.parseOrThrow(accountData.usernameLink.serverId.toByteArray())
)
SignalStore.misc.usernameQrCodeColorScheme = accountData.usernameLink.color.toLocalUsernameColor()
} else {
SignalStore.account.usernameLink = null
}
SignalDatabase.runPostSuccessfulTransaction { ProfileUtil.handleSelfProfileKeyChange() }
@ -222,7 +161,76 @@ object AccountDataProcessor {
Recipient.self().live().refresh()
}
private fun PhoneNumberPrivacyValues.PhoneNumberSharingMode.toBackupPhoneNumberSharingMode(): AccountData.PhoneNumberSharingMode {
private fun importSettings(context: Context, settings: AccountData.AccountSettings, importState: ImportState) {
TextSecurePreferences.setReadReceiptsEnabled(context, settings.readReceipts)
TextSecurePreferences.setTypingIndicatorsEnabled(context, settings.typingIndicators)
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, settings.sealedSenderIndicators)
SignalStore.settings.isLinkPreviewsEnabled = settings.linkPreviews
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.universalExpireTimerSeconds
SignalStore.emoji.reactions = settings.preferredReactionEmoji
SignalStore.inAppPayments.setDisplayBadgesOnProfile(settings.displayBadgesOnProfile)
SignalStore.settings.setKeepMutedChatsArchived(settings.keepMutedChatsArchived)
SignalStore.story.userHasBeenNotifiedAboutStories = settings.hasSetMyStoriesPrivacy
SignalStore.story.userHasViewedOnboardingStory = settings.hasViewedOnboardingStory
SignalStore.story.isFeatureDisabled = settings.storiesDisabled
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 (settings.defaultChatStyle != null) {
val chatColors = settings.defaultChatStyle.toLocal(importState)
SignalStore.chatColors.chatColors = chatColors
val wallpaperAttachmentId: AttachmentId? = settings.defaultChatStyle.wallpaperPhoto?.let { filePointer ->
filePointer.toLocalAttachment(importState)?.let {
SignalDatabase.attachments.restoreWallpaperAttachment(it)
}
}
SignalStore.wallpaper.wallpaper = settings.defaultChatStyle.parseChatWallpaper(wallpaperAttachmentId)
} else {
SignalStore.chatColors.chatColors = null
SignalStore.wallpaper.wallpaper = null
}
if (settings.preferredReactionEmoji.isNotEmpty()) {
SignalStore.emoji.reactions = settings.preferredReactionEmoji
}
if (settings.hasCompletedUsernameOnboarding) {
SignalStore.uiHints.setHasCompletedUsernameOnboarding(true)
}
}
private fun PhoneNumberPrivacyValues.PhoneNumberSharingMode.toRemotePhoneNumberSharingMode(): AccountData.PhoneNumberSharingMode {
return when (this) {
PhoneNumberPrivacyValues.PhoneNumberSharingMode.DEFAULT -> AccountData.PhoneNumberSharingMode.EVERYBODY
PhoneNumberPrivacyValues.PhoneNumberSharingMode.EVERYBODY -> AccountData.PhoneNumberSharingMode.EVERYBODY
@ -252,7 +260,7 @@ object AccountDataProcessor {
}
}
private fun UsernameQrCodeColorScheme.toBackupUsernameColor(): AccountData.UsernameLink.Color {
private fun UsernameQrCodeColorScheme.toRemoteUsernameColor(): AccountData.UsernameLink.Color {
return when (this) {
UsernameQrCodeColorScheme.Blue -> AccountData.UsernameLink.Color.BLUE
UsernameQrCodeColorScheme.White -> AccountData.UsernameLink.Color.WHITE

View file

@ -14,6 +14,9 @@ import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.database.SignalDatabase
/**
* Handles importing/exporting [AdHocCall] frames for an archive.
*/
object AdHocCallBackupProcessor {
val TAG = Log.tag(AdHocCallBackupProcessor::class.java)
@ -21,9 +24,7 @@ object AdHocCallBackupProcessor {
fun export(db: SignalDatabase, emitter: BackupFrameEmitter) {
db.callTable.getAdhocCallsForBackup().use { reader ->
for (callLog in reader) {
if (callLog != null) {
emitter.emit(Frame(adHocCall = callLog))
}
emitter.emit(Frame(adHocCall = callLog))
}
}
}

View file

@ -16,6 +16,9 @@ import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Handles importing/exporting [Chat] frames for an archive.
*/
object ChatBackupProcessor {
val TAG = Log.tag(ChatBackupProcessor::class.java)

View file

@ -11,17 +11,21 @@ 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
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.database.SignalDatabase
/**
* Handles importing/exporting [ChatItem] frames for an archive.
*/
object ChatItemBackupProcessor {
val TAG = Log.tag(ChatItemBackupProcessor::class.java)
fun export(db: SignalDatabase, exportState: ExportState, emitter: BackupFrameEmitter) {
db.messageTable.getMessagesForBackup(exportState.backupTime, exportState.mediaBackupEnabled).use { chatItems ->
db.messageTable.getMessagesForBackup(db, exportState.backupTime, exportState.mediaBackupEnabled).use { chatItems ->
while (chatItems.hasNext()) {
val chatItem = chatItems.next()
val chatItem: ChatItem? = chatItems.next()
if (chatItem != null) {
if (exportState.threadIds.contains(chatItem.chatId)) {
emitter.emit(Frame(chatItem = chatItem))

View file

@ -6,9 +6,9 @@
package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.ArchiveRecipient
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
import org.thoughtcrime.securesms.backup.v2.database.getContactsForBackup
@ -24,6 +24,9 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Handles importing/exporting [ArchiveRecipient] frames for an archive.
*/
object RecipientBackupProcessor {
val TAG = Log.tag(RecipientBackupProcessor::class.java)
@ -35,7 +38,7 @@ object RecipientBackupProcessor {
exportState.recipientIds.add(releaseChannelId.toLong())
emitter.emit(
Frame(
recipient = BackupRecipient(
recipient = ArchiveRecipient(
id = releaseChannelId.toLong(),
releaseNotes = ReleaseNotes()
)
@ -46,33 +49,35 @@ object RecipientBackupProcessor {
}
db.recipientTable.getContactsForBackup(selfId).use { reader ->
for (backupRecipient in reader) {
if (backupRecipient != null) {
exportState.recipientIds.add(backupRecipient.id)
emitter.emit(Frame(recipient = backupRecipient))
}
for (recipient in reader) {
exportState.recipientIds.add(recipient.id)
emitter.emit(Frame(recipient = recipient))
}
}
db.recipientTable.getGroupsForBackup().use { reader ->
for (backupRecipient in reader) {
exportState.recipientIds.add(backupRecipient.id)
emitter.emit(Frame(recipient = backupRecipient))
for (recipient in reader) {
exportState.recipientIds.add(recipient.id)
emitter.emit(Frame(recipient = recipient))
}
}
db.distributionListTables.getAllForBackup().forEach {
exportState.recipientIds.add(it.id)
emitter.emit(Frame(recipient = it))
db.distributionListTables.getAllForBackup().use { reader ->
for (recipient in reader) {
exportState.recipientIds.add(recipient.id)
emitter.emit(Frame(recipient = recipient))
}
}
db.callLinkTable.getCallLinksForBackup().forEach {
exportState.recipientIds.add(it.id)
emitter.emit(Frame(recipient = it))
db.callLinkTable.getCallLinksForBackup().use { reader ->
for (recipient in reader) {
exportState.recipientIds.add(recipient.id)
emitter.emit(Frame(recipient = recipient))
}
}
}
fun import(recipient: BackupRecipient, importState: ImportState) {
fun import(recipient: ArchiveRecipient, importState: ImportState) {
val newId = when {
recipient.contact != null -> SignalDatabase.recipients.restoreContactFromBackup(recipient.contact)
recipient.group != null -> SignalDatabase.recipients.restoreGroupFromBackup(recipient.group)

View file

@ -19,6 +19,9 @@ import org.thoughtcrime.securesms.database.model.StickerPackRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob
/**
* Handles importing/exporting [StickerPack] frames for an archive.
*/
object StickerBackupProcessor {
fun export(db: SignalDatabase, emitter: BackupFrameEmitter) {
StickerPackRecordReader(db.stickerTable.allStickerPacks).use { reader ->