Refactor archive importing.

This commit is contained in:
Greyson Parrelli
2024-10-01 14:07:08 -04:00
parent 9d0aef8dbc
commit ac0e80ca05
41 changed files with 808 additions and 705 deletions

View File

@@ -7,3 +7,4 @@ package org.thoughtcrime.securesms.backup.v2
typealias ArchiveRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
typealias ArchiveGroup = org.thoughtcrime.securesms.backup.v2.proto.Group
typealias ArchiveCallLink = org.thoughtcrime.securesms.backup.v2.proto.CallLink

View File

@@ -27,14 +27,14 @@ import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
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.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
import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.StickerBackupProcessor
import org.thoughtcrime.securesms.backup.v2.importer.ChatItemArchiveImporter
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.processor.ChatArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.processor.RecipientArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.processor.StickerArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter
import org.thoughtcrime.securesms.backup.v2.stream.BackupImportReader
@@ -285,37 +285,37 @@ object BackupRepository {
// We're using a snapshot, so the transaction is more for perf than correctness
dbSnapshot.rawWritableDatabase.withinTransaction {
progressEmitter?.onAccount()
AccountDataBackupProcessor.export(dbSnapshot, signalStoreSnapshot) {
AccountDataArchiveProcessor.export(dbSnapshot, signalStoreSnapshot) {
writer.write(it)
eventTimer.emit("account")
}
progressEmitter?.onRecipient()
RecipientBackupProcessor.export(dbSnapshot, signalStoreSnapshot, exportState) {
RecipientArchiveProcessor.export(dbSnapshot, signalStoreSnapshot, exportState) {
writer.write(it)
eventTimer.emit("recipient")
}
progressEmitter?.onThread()
ChatBackupProcessor.export(dbSnapshot, exportState) { frame ->
ChatArchiveProcessor.export(dbSnapshot, exportState) { frame ->
writer.write(frame)
eventTimer.emit("thread")
}
progressEmitter?.onCall()
AdHocCallBackupProcessor.export(dbSnapshot) { frame ->
AdHocCallArchiveProcessor.export(dbSnapshot) { frame ->
writer.write(frame)
eventTimer.emit("call")
}
progressEmitter?.onSticker()
StickerBackupProcessor.export(dbSnapshot) { frame ->
StickerArchiveProcessor.export(dbSnapshot) { frame ->
writer.write(frame)
eventTimer.emit("sticker-pack")
}
progressEmitter?.onMessage()
ChatItemBackupProcessor.export(dbSnapshot, exportState) { frame ->
ChatItemArchiveProcessor.export(dbSnapshot, exportState) { frame ->
writer.write(frame)
eventTimer.emit("message")
}
@@ -406,38 +406,38 @@ object BackupRepository {
eventTimer.emit("setup")
val importState = ImportState(backupKey)
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(importState)
val chatItemInserter: ChatItemArchiveImporter = ChatItemArchiveProcessor.beginImport(importState)
val totalLength = frameReader.getStreamLength()
for (frame in frameReader) {
when {
frame.account != null -> {
AccountDataBackupProcessor.import(frame.account, selfId, importState)
AccountDataArchiveProcessor.import(frame.account, selfId, importState)
eventTimer.emit("account")
}
frame.recipient != null -> {
RecipientBackupProcessor.import(frame.recipient, importState)
RecipientArchiveProcessor.import(frame.recipient, importState)
eventTimer.emit("recipient")
}
frame.chat != null -> {
ChatBackupProcessor.import(frame.chat, importState)
ChatArchiveProcessor.import(frame.chat, importState)
eventTimer.emit("chat")
}
frame.adHocCall != null -> {
AdHocCallBackupProcessor.import(frame.adHocCall, importState)
AdHocCallArchiveProcessor.import(frame.adHocCall, importState)
eventTimer.emit("call")
}
frame.stickerPack != null -> {
StickerBackupProcessor.import(frame.stickerPack)
StickerArchiveProcessor.import(frame.stickerPack)
eventTimer.emit("sticker-pack")
}
frame.chatItem != null -> {
chatItemInserter.insert(frame.chatItem)
chatItemInserter.import(frame.chatItem)
eventTimer.emit("chatItem")
// TODO if there's stuff in the stream after chatItems, we need to flush the inserter before going to the next phase
}

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.select
import org.thoughtcrime.securesms.database.CallLinkTable
fun CallLinkTable.getCallLinksForBackup(): CallLinkArchiveExporter {
val cursor = readableDatabase
.select()
.from(CallLinkTable.TABLE_NAME)
.run()
return CallLinkArchiveExporter(cursor)
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
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.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.time.Instant
fun CallLinkTable.getCallLinksForBackup(): CallLinkArchiveExportIterator {
val cursor = readableDatabase
.select()
.from(CallLinkTable.TABLE_NAME)
.run()
return CallLinkArchiveExportIterator(cursor)
}
fun CallLinkTable.restoreFromBackup(callLink: CallLink): RecipientId? {
val rootKey: CallLinkRootKey
try {
rootKey = CallLinkRootKey(callLink.rootKey.toByteArray())
} catch (e: Exception) {
return null
}
return SignalDatabase.callLinks.insertCallLink(
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = CallLinkRoomId.fromCallLinkRootKey(rootKey),
credentials = CallLinkCredentials(callLink.rootKey.toByteArray(), callLink.adminKey?.toByteArray()),
state = SignalCallLinkState(
name = callLink.name,
restrictions = callLink.restrictions.toLocal(),
expiration = Instant.ofEpochMilli(callLink.expirationMs)
),
deletionTimestamp = 0L
)
)
}
private fun CallLink.Restrictions.toLocal(): CallLinkState.Restrictions {
return when (this) {
CallLink.Restrictions.ADMIN_APPROVAL -> CallLinkState.Restrictions.ADMIN_APPROVAL
CallLink.Restrictions.NONE -> CallLinkState.Restrictions.NONE
CallLink.Restrictions.UNKNOWN -> CallLinkState.Restrictions.UNKNOWN
}
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.select
import org.thoughtcrime.securesms.database.CallTable
fun CallTable.getAdhocCallsForBackup(): AdHocCallArchiveExporter {
return AdHocCallArchiveExporter(
readableDatabase
.select()
.from(CallTable.TABLE_NAME)
.where("${CallTable.TYPE} = ?", CallTable.Type.serialize(CallTable.Type.AD_HOC_CALL))
.run()
)
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.insertInto
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
fun CallTable.getAdhocCallsForBackup(): AdHocCallArchiveExportIterator {
return AdHocCallArchiveExportIterator(
readableDatabase
.select()
.from(CallTable.TABLE_NAME)
.where("${CallTable.TYPE} = ?", CallTable.Type.serialize(CallTable.Type.AD_HOC_CALL))
.run()
)
}
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
}
writableDatabase
.insertInto(CallTable.TABLE_NAME)
.values(
CallTable.CALL_ID to call.callId,
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),
CallTable.TIMESTAMP to call.callTimestamp
)
.run()
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.deleteAll
import org.signal.core.util.select
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.backup.v2.exporters.DistributionListArchiveExporter
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.recipients.RecipientId
fun DistributionListTables.getAllForBackup(): DistributionListArchiveExporter {
val cursor = readableDatabase
.select()
.from(DistributionListTables.ListTable.TABLE_NAME)
.run()
return DistributionListArchiveExporter(cursor, this)
}
fun DistributionListTables.getMembersForBackup(id: DistributionListId): List<RecipientId> {
lateinit var privacyMode: DistributionListPrivacyMode
lateinit var rawMembers: List<RecipientId>
readableDatabase.withinTransaction {
privacyMode = getPrivacyMode(id)
rawMembers = getRawMembers(id, privacyMode)
}
return when (privacyMode) {
DistributionListPrivacyMode.ALL -> emptyList()
DistributionListPrivacyMode.ONLY_WITH -> rawMembers
DistributionListPrivacyMode.ALL_EXCEPT -> rawMembers
}
}
fun DistributionListTables.clearAllDataForBackupRestore() {
writableDatabase.deleteAll(DistributionListTables.ListTable.TABLE_NAME)
writableDatabase.deleteAll(DistributionListTables.MembershipTable.TABLE_NAME)
}

View File

@@ -1,102 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.deleteAll
import org.signal.core.util.logging.Log
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.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.recipients.RecipientId
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.util.UuidUtil
import org.thoughtcrime.securesms.backup.v2.proto.DistributionList as BackupDistributionList
private val TAG = Log.tag(DistributionListTables::class.java)
fun DistributionListTables.getAllForBackup(): DistributionListArchiveExportIterator {
val cursor = readableDatabase
.select()
.from(DistributionListTables.ListTable.TABLE_NAME)
.run()
return DistributionListArchiveExportIterator(cursor, this)
}
fun DistributionListTables.getMembersForBackup(id: DistributionListId): List<RecipientId> {
lateinit var privacyMode: DistributionListPrivacyMode
lateinit var rawMembers: List<RecipientId>
readableDatabase.withinTransaction {
privacyMode = getPrivacyMode(id)
rawMembers = getRawMembers(id, privacyMode)
}
return when (privacyMode) {
DistributionListPrivacyMode.ALL -> emptyList()
DistributionListPrivacyMode.ONLY_WITH -> rawMembers
DistributionListPrivacyMode.ALL_EXCEPT -> rawMembers
}
}
fun DistributionListTables.restoreFromBackup(dlistItem: DistributionListItem, importState: ImportState): RecipientId? {
if (dlistItem.deletionTimestamp != null && dlistItem.deletionTimestamp > 0) {
val dlistId = createList(
name = "",
members = emptyList(),
distributionId = DistributionId.from(UuidUtil.fromByteString(dlistItem.distributionId)),
allowsReplies = false,
deletionTimestamp = dlistItem.deletionTimestamp,
storageId = null,
privacyMode = DistributionListPrivacyMode.ONLY_WITH
)!!
return SignalDatabase.distributionLists.getRecipientId(dlistId)!!
}
val dlist = dlistItem.distributionList ?: return null
val members: List<RecipientId> = dlist.memberRecipientIds
.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()}")
}
val distributionId = DistributionId.from(UuidUtil.fromByteString(dlistItem.distributionId))
val privacyMode = dlist.privacyMode.toLocalPrivacyMode()
val dlistId = createList(
name = dlist.name,
members = members,
distributionId = distributionId,
allowsReplies = dlist.allowReplies,
deletionTimestamp = dlistItem.deletionTimestamp ?: 0,
storageId = null,
privacyMode = privacyMode
)!!
return SignalDatabase.distributionLists.getRecipientId(dlistId)!!
}
fun DistributionListTables.clearAllDataForBackupRestore() {
writableDatabase.deleteAll(DistributionListTables.ListTable.TABLE_NAME)
writableDatabase.deleteAll(DistributionListTables.MembershipTable.TABLE_NAME)
}
private fun BackupDistributionList.PrivacyMode.toLocalPrivacyMode(): DistributionListPrivacyMode {
return when (this) {
BackupDistributionList.PrivacyMode.UNKNOWN -> DistributionListPrivacyMode.ALL
BackupDistributionList.PrivacyMode.ONLY_WITH -> DistributionListPrivacyMode.ONLY_WITH
BackupDistributionList.PrivacyMode.ALL -> DistributionListPrivacyMode.ALL
BackupDistributionList.PrivacyMode.ALL_EXCEPT -> DistributionListPrivacyMode.ALL_EXCEPT
}
}

View File

@@ -8,7 +8,8 @@ package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.SqlUtil
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.backup.v2.exporters.ChatItemArchiveExporter
import org.thoughtcrime.securesms.backup.v2.importer.ChatItemArchiveImporter
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -16,7 +17,7 @@ import java.util.concurrent.TimeUnit
private const val COLUMN_BASE_TYPE = "base_type"
fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, mediaBackupEnabled: Boolean): ChatItemArchiveExportIterator {
fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, mediaBackupEnabled: Boolean): ChatItemArchiveExporter {
val cursor = readableDatabase
.select(
MessageTable.ID,
@@ -66,11 +67,11 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, medi
.orderBy("${MessageTable.DATE_RECEIVED} ASC")
.run()
return ChatItemArchiveExportIterator(db, cursor, 100, mediaBackupEnabled)
return ChatItemArchiveExporter(db, cursor, 100, mediaBackupEnabled)
}
fun MessageTable.createChatItemInserter(importState: ImportState): ChatItemImportInserter {
return ChatItemImportInserter(writableDatabase, importState, 100)
fun MessageTable.createChatItemInserter(importState: ImportState): ChatItemArchiveImporter {
return ChatItemArchiveImporter(writableDatabase, importState, 100)
}
fun MessageTable.clearAllDataForBackupRestore() {

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import android.content.ContentValues
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.select
import org.signal.core.util.update
import org.signal.libsignal.zkgroup.InvalidInputException
import org.thoughtcrime.securesms.backup.v2.exporters.ContactArchiveExporter
import org.thoughtcrime.securesms.backup.v2.exporters.GroupArchiveExporter
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* 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): ContactArchiveExporter {
val cursor = readableDatabase
.select(
RecipientTable.ID,
RecipientTable.ACI_COLUMN,
RecipientTable.PNI_COLUMN,
RecipientTable.USERNAME,
RecipientTable.E164,
RecipientTable.BLOCKED,
RecipientTable.HIDDEN,
RecipientTable.REGISTERED,
RecipientTable.UNREGISTERED_TIMESTAMP,
RecipientTable.PROFILE_KEY,
RecipientTable.PROFILE_SHARING,
RecipientTable.PROFILE_GIVEN_NAME,
RecipientTable.PROFILE_FAMILY_NAME,
RecipientTable.PROFILE_JOINED_NAME,
RecipientTable.MUTE_UNTIL,
RecipientTable.CHAT_COLORS,
RecipientTable.CUSTOM_CHAT_COLORS_ID,
RecipientTable.EXTRAS
)
.from(RecipientTable.TABLE_NAME)
.where(
"""
${RecipientTable.TYPE} = ? AND (
${RecipientTable.ACI_COLUMN} NOT NULL OR
${RecipientTable.PNI_COLUMN} NOT NULL OR
${RecipientTable.E164} NOT NULL
)
""",
RecipientTable.RecipientType.INDIVIDUAL.id
)
.run()
return ContactArchiveExporter(cursor, selfId)
}
fun RecipientTable.getGroupsForBackup(): GroupArchiveExporter {
val cursor = readableDatabase
.select(
"${RecipientTable.TABLE_NAME}.${RecipientTable.ID}",
"${RecipientTable.TABLE_NAME}.${RecipientTable.BLOCKED}",
"${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_SHARING}",
"${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL}",
"${RecipientTable.TABLE_NAME}.${RecipientTable.EXTRAS}",
"${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY}",
"${GroupTable.TABLE_NAME}.${GroupTable.SHOW_AS_STORY_STATE}",
"${GroupTable.TABLE_NAME}.${GroupTable.TITLE}",
"${GroupTable.TABLE_NAME}.${GroupTable.V2_DECRYPTED_GROUP}"
)
.from(
"""
${RecipientTable.TABLE_NAME}
INNER JOIN ${GroupTable.TABLE_NAME} ON ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} = ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID}
"""
)
.where("${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY} IS NOT NULL")
.run()
return GroupArchiveExporter(cursor)
}
/**
* Given [AccountData], this will insert the necessary data for the local user into the [RecipientTable].
*/
fun RecipientTable.restoreSelfFromBackup(accountData: AccountData, selfId: RecipientId) {
val values = ContentValues().apply {
put(RecipientTable.PROFILE_GIVEN_NAME, accountData.givenName.nullIfBlank())
put(RecipientTable.PROFILE_FAMILY_NAME, accountData.familyName.nullIfBlank())
put(RecipientTable.PROFILE_JOINED_NAME, ProfileName.fromParts(accountData.givenName, accountData.familyName).toString().nullIfBlank())
put(RecipientTable.PROFILE_AVATAR, accountData.avatarUrlPath.nullIfBlank())
put(RecipientTable.REGISTERED, RecipientTable.RegisteredState.REGISTERED.id)
put(RecipientTable.PROFILE_SHARING, true)
put(RecipientTable.UNREGISTERED_TIMESTAMP, 0)
put(RecipientTable.EXTRAS, RecipientExtras().encode())
try {
put(RecipientTable.PROFILE_KEY, Base64.encodeWithPadding(accountData.profileKey.toByteArray()).nullIfBlank())
} catch (e: InvalidInputException) {
Log.w(TAG, "Missing profile key during restore")
}
put(RecipientTable.USERNAME, accountData.username)
}
writableDatabase
.update(RecipientTable.TABLE_NAME)
.values(values)
.where("${RecipientTable.ID} = ?", selfId)
.run()
}
fun RecipientTable.clearAllDataForBackupRestore() {
writableDatabase.deleteAll(RecipientTable.TABLE_NAME)
SqlUtil.resetAutoIncrementValue(writableDatabase, RecipientTable.TABLE_NAME)
RecipientId.clearCache()
AppDependencies.recipientCache.clear()
AppDependencies.recipientCache.clearSelf()
}
fun RecipientTable.restoreReleaseNotes(): RecipientId {
val releaseChannelId: RecipientId = insertReleaseChannelRecipient()
SignalStore.releaseChannel.setReleaseChannelRecipientId(releaseChannelId)
setProfileName(releaseChannelId, ProfileName.asGiven("Signal"))
setMuted(releaseChannelId, Long.MAX_VALUE)
return releaseChannelId
}

View File

@@ -1,336 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import android.content.ContentValues
import androidx.core.content.contentValuesOf
import org.signal.core.util.Base64
import org.signal.core.util.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.select
import org.signal.core.util.toInt
import org.signal.core.util.update
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.groups.GroupSecretParams
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.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.conversation.colors.AvatarColorHash
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
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
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): ContactArchiveExportIterator {
val cursor = readableDatabase
.select(
RecipientTable.ID,
RecipientTable.ACI_COLUMN,
RecipientTable.PNI_COLUMN,
RecipientTable.USERNAME,
RecipientTable.E164,
RecipientTable.BLOCKED,
RecipientTable.HIDDEN,
RecipientTable.REGISTERED,
RecipientTable.UNREGISTERED_TIMESTAMP,
RecipientTable.PROFILE_KEY,
RecipientTable.PROFILE_SHARING,
RecipientTable.PROFILE_GIVEN_NAME,
RecipientTable.PROFILE_FAMILY_NAME,
RecipientTable.PROFILE_JOINED_NAME,
RecipientTable.MUTE_UNTIL,
RecipientTable.CHAT_COLORS,
RecipientTable.CUSTOM_CHAT_COLORS_ID,
RecipientTable.EXTRAS
)
.from(RecipientTable.TABLE_NAME)
.where(
"""
${RecipientTable.TYPE} = ? AND (
${RecipientTable.ACI_COLUMN} NOT NULL OR
${RecipientTable.PNI_COLUMN} NOT NULL OR
${RecipientTable.E164} NOT NULL
)
""",
RecipientTable.RecipientType.INDIVIDUAL.id
)
.run()
return ContactArchiveExportIterator(cursor, selfId)
}
fun RecipientTable.getGroupsForBackup(): GroupArchiveExportIterator {
val cursor = readableDatabase
.select(
"${RecipientTable.TABLE_NAME}.${RecipientTable.ID}",
"${RecipientTable.TABLE_NAME}.${RecipientTable.BLOCKED}",
"${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_SHARING}",
"${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL}",
"${RecipientTable.TABLE_NAME}.${RecipientTable.EXTRAS}",
"${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY}",
"${GroupTable.TABLE_NAME}.${GroupTable.SHOW_AS_STORY_STATE}",
"${GroupTable.TABLE_NAME}.${GroupTable.TITLE}",
"${GroupTable.TABLE_NAME}.${GroupTable.V2_DECRYPTED_GROUP}"
)
.from(
"""
${RecipientTable.TABLE_NAME}
INNER JOIN ${GroupTable.TABLE_NAME} ON ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} = ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID}
"""
)
.where("${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY} IS NOT NULL")
.run()
return GroupArchiveExportIterator(cursor)
}
/**
* Given [AccountData], this will insert the necessary data for the local user into the [RecipientTable].
*/
fun RecipientTable.restoreSelfFromBackup(accountData: AccountData, selfId: RecipientId) {
val values = ContentValues().apply {
put(RecipientTable.PROFILE_GIVEN_NAME, accountData.givenName.nullIfBlank())
put(RecipientTable.PROFILE_FAMILY_NAME, accountData.familyName.nullIfBlank())
put(RecipientTable.PROFILE_JOINED_NAME, ProfileName.fromParts(accountData.givenName, accountData.familyName).toString().nullIfBlank())
put(RecipientTable.PROFILE_AVATAR, accountData.avatarUrlPath.nullIfBlank())
put(RecipientTable.REGISTERED, RecipientTable.RegisteredState.REGISTERED.id)
put(RecipientTable.PROFILE_SHARING, true)
put(RecipientTable.UNREGISTERED_TIMESTAMP, 0)
put(RecipientTable.EXTRAS, RecipientExtras().encode())
try {
put(RecipientTable.PROFILE_KEY, Base64.encodeWithPadding(accountData.profileKey.toByteArray()).nullIfBlank())
} catch (e: InvalidInputException) {
Log.w(TAG, "Missing profile key during restore")
}
put(RecipientTable.USERNAME, accountData.username)
}
writableDatabase
.update(RecipientTable.TABLE_NAME)
.values(values)
.where("${RecipientTable.ID} = ?", selfId)
.run()
}
fun RecipientTable.clearAllDataForBackupRestore() {
writableDatabase.deleteAll(RecipientTable.TABLE_NAME)
SqlUtil.resetAutoIncrementValue(writableDatabase, RecipientTable.TABLE_NAME)
RecipientId.clearCache()
AppDependencies.recipientCache.clear()
AppDependencies.recipientCache.clearSelf()
}
fun RecipientTable.restoreContactFromBackup(contact: Contact): RecipientId {
val id = getAndPossiblyMergePnpVerified(
aci = ACI.parseOrNull(contact.aci?.toByteArray()),
pni = PNI.parseOrNull(contact.pni?.toByteArray()),
e164 = contact.formattedE164
)
val profileKey = contact.profileKey?.toByteArray()
val values = contentValuesOf(
RecipientTable.BLOCKED to contact.blocked,
RecipientTable.HIDDEN to contact.visibility.toLocal().serialize(),
RecipientTable.TYPE to RecipientTable.RecipientType.INDIVIDUAL.id,
RecipientTable.PROFILE_FAMILY_NAME to contact.profileFamilyName,
RecipientTable.PROFILE_GIVEN_NAME to contact.profileGivenName,
RecipientTable.PROFILE_JOINED_NAME to ProfileName.fromParts(contact.profileGivenName, contact.profileFamilyName).toString(),
RecipientTable.PROFILE_KEY to if (profileKey == null) null else Base64.encodeWithPadding(profileKey),
RecipientTable.PROFILE_SHARING to contact.profileSharing.toInt(),
RecipientTable.USERNAME to contact.username,
RecipientTable.EXTRAS to contact.toLocalExtras().encode()
)
if (contact.registered != null) {
values.put(RecipientTable.UNREGISTERED_TIMESTAMP, 0L)
values.put(RecipientTable.REGISTERED, RecipientTable.RegisteredState.REGISTERED.id)
} else if (contact.notRegistered != null) {
values.put(RecipientTable.UNREGISTERED_TIMESTAMP, contact.notRegistered.unregisteredTimestamp)
values.put(RecipientTable.REGISTERED, RecipientTable.RegisteredState.NOT_REGISTERED.id)
}
writableDatabase
.update(RecipientTable.TABLE_NAME)
.values(values)
.where("${RecipientTable.ID} = ?", id)
.run()
return id
}
fun RecipientTable.restoreReleaseNotes(): RecipientId {
val releaseChannelId: RecipientId = insertReleaseChannelRecipient()
SignalStore.releaseChannel.setReleaseChannelRecipientId(releaseChannelId)
setProfileName(releaseChannelId, ProfileName.asGiven("Signal"))
setMuted(releaseChannelId, Long.MAX_VALUE)
return releaseChannelId
}
fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId {
val masterKey = GroupMasterKey(group.masterKey.toByteArray())
val groupId = GroupId.v2(masterKey)
val operations = AppDependencies.groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(masterKey))
val decryptedState = if (group.snapshot == null) {
DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
} else {
group.snapshot.toLocal(operations)
}
val values = ContentValues().apply {
put(RecipientTable.GROUP_ID, groupId.toString())
put(RecipientTable.AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize())
put(RecipientTable.PROFILE_SHARING, group.whitelisted)
put(RecipientTable.TYPE, RecipientTable.RecipientType.GV2.id)
put(RecipientTable.STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
if (group.hideStory) {
val extras = RecipientExtras.Builder().hideStory(true).build()
put(RecipientTable.EXTRAS, extras.encode())
}
}
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.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
Contact.Visibility.HIDDEN -> Recipient.HiddenState.HIDDEN
Contact.Visibility.HIDDEN_MESSAGE_REQUEST -> Recipient.HiddenState.HIDDEN_MESSAGE_REQUEST
}
}
private fun Group.AccessControl.AccessRequired.toLocal(): AccessControl.AccessRequired {
return when (this) {
Group.AccessControl.AccessRequired.UNKNOWN -> AccessControl.AccessRequired.UNKNOWN
Group.AccessControl.AccessRequired.ANY -> AccessControl.AccessRequired.ANY
Group.AccessControl.AccessRequired.MEMBER -> AccessControl.AccessRequired.MEMBER
Group.AccessControl.AccessRequired.ADMINISTRATOR -> AccessControl.AccessRequired.ADMINISTRATOR
Group.AccessControl.AccessRequired.UNSATISFIABLE -> AccessControl.AccessRequired.UNSATISFIABLE
}
}
private fun Group.AccessControl.toLocal(): AccessControl {
return AccessControl(members = this.members.toLocal(), attributes = this.attributes.toLocal(), addFromInviteLink = this.addFromInviteLink.toLocal())
}
private fun Group.Member.Role.toLocal(): Member.Role {
return when (this) {
Group.Member.Role.UNKNOWN -> Member.Role.UNKNOWN
Group.Member.Role.DEFAULT -> Member.Role.DEFAULT
Group.Member.Role.ADMINISTRATOR -> Member.Role.ADMINISTRATOR
}
}
private fun Group.Member.toLocal(): DecryptedMember {
return DecryptedMember(aciBytes = userId, role = role.toLocal(), joinedAtRevision = joinedAtVersion)
}
private fun Group.MemberPendingAdminApproval.toLocal(): DecryptedRequestingMember {
return DecryptedRequestingMember(
aciBytes = this.userId,
timestamp = this.timestamp
)
}
private fun Group.MemberBanned.toLocal(): DecryptedBannedMember {
return DecryptedBannedMember(
serviceIdBytes = this.userId,
timestamp = this.timestamp
)
}
private fun Group.GroupSnapshot.toLocal(operations: GroupsV2Operations.GroupOperations): DecryptedGroup {
return DecryptedGroup(
title = this.title?.title ?: "",
avatar = this.avatarUrl,
disappearingMessagesTimer = DecryptedTimer(duration = this.disappearingMessagesTimer?.disappearingMessagesDuration ?: 0),
accessControl = this.accessControl?.toLocal(),
revision = this.version,
members = this.members.map { member -> member.toLocal() },
pendingMembers = this.membersPendingProfileKey.map { pending -> pending.toLocal(operations) },
requestingMembers = this.membersPendingAdminApproval.map { requesting -> requesting.toLocal() },
inviteLinkPassword = this.inviteLinkPassword,
description = this.description?.descriptionText ?: "",
isAnnouncementGroup = if (this.announcements_only) EnabledState.ENABLED else EnabledState.DISABLED,
bannedMembers = this.members_banned.map { it.toLocal() }
)
}
private fun Contact.toLocalExtras(): RecipientExtras {
return RecipientExtras(
hideStory = this.hideStory
)
}
private val Contact.formattedE164: String?
get() {
return e164?.let {
PhoneNumberFormatter.get(AppDependencies.application).format(e164.toString())
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.SqlUtil
import org.thoughtcrime.securesms.backup.v2.exporters.ChatArchiveExporter
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
fun ThreadTable.getThreadsForBackup(db: SignalDatabase): ChatArchiveExporter {
//language=sql
val query = """
SELECT
${ThreadTable.TABLE_NAME}.${ThreadTable.ID},
${ThreadTable.RECIPIENT_ID},
${ThreadTable.PINNED},
${ThreadTable.READ},
${ThreadTable.ARCHIVED},
${RecipientTable.TABLE_NAME}.${RecipientTable.MESSAGE_EXPIRATION_TIME},
${RecipientTable.TABLE_NAME}.${RecipientTable.MESSAGE_EXPIRATION_TIME_VERSION},
${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL},
${RecipientTable.TABLE_NAME}.${RecipientTable.MENTION_SETTING},
${RecipientTable.TABLE_NAME}.${RecipientTable.CHAT_COLORS},
${RecipientTable.TABLE_NAME}.${RecipientTable.CUSTOM_CHAT_COLORS_ID},
${RecipientTable.TABLE_NAME}.${RecipientTable.WALLPAPER}
FROM ${ThreadTable.TABLE_NAME}
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
WHERE ${ThreadTable.ACTIVE} = 1
"""
val cursor = readableDatabase.query(query)
return ChatArchiveExporter(cursor, db)
}
fun ThreadTable.clearAllDataForBackupRestore() {
writableDatabase.delete(ThreadTable.TABLE_NAME, null, null)
SqlUtil.resetAutoIncrementValue(writableDatabase, ThreadTable.TABLE_NAME)
clearCache()
}

View File

@@ -1,100 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import androidx.core.content.contentValuesOf
import org.signal.core.util.SqlUtil
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
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.parseChatWallpaper
import org.thoughtcrime.securesms.backup.v2.util.toLocal
import org.thoughtcrime.securesms.backup.v2.util.toLocalAttachment
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.recipients.RecipientId
import org.thoughtcrime.securesms.wallpaper.UriChatWallpaper
private val TAG = Log.tag(ThreadTable::class.java)
fun ThreadTable.getThreadsForBackup(db: SignalDatabase): ChatArchiveExportIterator {
//language=sql
val query = """
SELECT
${ThreadTable.TABLE_NAME}.${ThreadTable.ID},
${ThreadTable.RECIPIENT_ID},
${ThreadTable.PINNED},
${ThreadTable.READ},
${ThreadTable.ARCHIVED},
${RecipientTable.TABLE_NAME}.${RecipientTable.MESSAGE_EXPIRATION_TIME},
${RecipientTable.TABLE_NAME}.${RecipientTable.MESSAGE_EXPIRATION_TIME_VERSION},
${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL},
${RecipientTable.TABLE_NAME}.${RecipientTable.MENTION_SETTING},
${RecipientTable.TABLE_NAME}.${RecipientTable.CHAT_COLORS},
${RecipientTable.TABLE_NAME}.${RecipientTable.CUSTOM_CHAT_COLORS_ID},
${RecipientTable.TABLE_NAME}.${RecipientTable.WALLPAPER}
FROM ${ThreadTable.TABLE_NAME}
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
WHERE ${ThreadTable.ACTIVE} = 1
"""
val cursor = readableDatabase.query(query)
return ChatArchiveExportIterator(cursor, db)
}
fun ThreadTable.clearAllDataForBackupRestore() {
writableDatabase.delete(ThreadTable.TABLE_NAME, null, null)
SqlUtil.resetAutoIncrementValue(writableDatabase, ThreadTable.TABLE_NAME)
clearCache()
}
fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importState: ImportState): Long {
val chatColor = chat.style?.toLocal(importState)
val wallpaperAttachmentId: AttachmentId? = chat.style?.wallpaperPhoto?.let { filePointer ->
filePointer.toLocalAttachment(importState)?.let {
SignalDatabase.attachments.restoreWallpaperAttachment(it)
}
}
val chatWallpaper = chat.style?.parseChatWallpaper(wallpaperAttachmentId)
val threadId = writableDatabase
.insertInto(ThreadTable.TABLE_NAME)
.values(
ThreadTable.RECIPIENT_ID to recipientId.serialize(),
ThreadTable.PINNED to chat.pinnedOrder,
ThreadTable.ARCHIVED to chat.archived.toInt(),
ThreadTable.READ to if (chat.markedUnread) ThreadTable.ReadStatus.FORCED_UNREAD.serialize() else ThreadTable.ReadStatus.READ.serialize(),
ThreadTable.ACTIVE to 1
)
.run()
writableDatabase
.update(
RecipientTable.TABLE_NAME,
contentValuesOf(
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.MESSAGE_EXPIRATION_TIME_VERSION to chat.expireTimerVersion,
RecipientTable.CHAT_COLORS to chatColor?.serialize()?.encode(),
RecipientTable.CUSTOM_CHAT_COLORS_ID to (chatColor?.id ?: ChatColors.Id.NotSet).longValue,
RecipientTable.WALLPAPER_URI to if (chatWallpaper is UriChatWallpaper) chatWallpaper.uri.toString() else null,
RecipientTable.WALLPAPER to chatWallpaper?.serialize()?.encode()
),
"${RecipientTable.ID} = ?",
SqlUtil.buildArgs(recipientId.toLong())
)
return threadId
}

View File

@@ -15,7 +15,7 @@ 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 {
class AdHocCallArchiveExporter(private val cursor: Cursor) : Iterator<AdHocCall>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}

View File

@@ -18,7 +18,7 @@ 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 {
class CallLinkArchiveExporter(private val cursor: Cursor) : Iterator<ArchiveRecipient>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}

View File

@@ -21,7 +21,7 @@ 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 {
class ChatArchiveExporter(private val cursor: Cursor, private val db: SignalDatabase) : Iterator<Chat>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}

View File

@@ -85,7 +85,7 @@ import kotlin.jvm.optionals.getOrNull
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange
import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
private val TAG = Log.tag(ChatItemArchiveExportIterator::class.java)
private val TAG = Log.tag(ChatItemArchiveExporter::class.java)
private const val COLUMN_BASE_TYPE = "base_type"
/**
@@ -95,7 +95,7 @@ private const val COLUMN_BASE_TYPE = "base_type"
*
* All of this complexity is hidden from the user -- they just get a normal iterator interface.
*/
class ChatItemArchiveExportIterator(
class ChatItemArchiveExporter(
private val db: SignalDatabase,
private val cursor: Cursor,
private val batchSize: Int,

View File

@@ -26,7 +26,7 @@ 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 {
class ContactArchiveExporter(private val cursor: Cursor, private val selfId: Long) : Iterator<ArchiveRecipient>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}

View File

@@ -24,7 +24,7 @@ import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.util.toByteArray
import java.io.Closeable
class DistributionListArchiveExportIterator(
class DistributionListArchiveExporter(
private val cursor: Cursor,
private val distributionListTables: DistributionListTables
) : Iterator<ArchiveRecipient>, Closeable {

View File

@@ -33,7 +33,7 @@ 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 {
class GroupArchiveExporter(private val cursor: Cursor) : Iterator<ArchiveRecipient>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.importer
import org.signal.core.util.insertInto
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.SignalDatabase
/**
* Handles the importing of [AdHocCall] models into the local database.
*/
object AdHodCallArchiveImporter {
fun import(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
}
SignalDatabase.writableDatabase
.insertInto(CallTable.TABLE_NAME)
.values(
CallTable.CALL_ID to call.callId,
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),
CallTable.TIMESTAMP to call.callTimestamp
)
.run()
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.importer
import org.signal.ringrtc.CallLinkRootKey
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.backup.v2.ArchiveCallLink
import org.thoughtcrime.securesms.backup.v2.proto.CallLink
import org.thoughtcrime.securesms.database.CallLinkTable
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.time.Instant
/**
* Handles the importing of [ArchiveCallLink] models into the local database.
*/
object CallLinkArchiveImporter {
fun import(callLink: ArchiveCallLink): RecipientId? {
val rootKey: CallLinkRootKey
try {
rootKey = CallLinkRootKey(callLink.rootKey.toByteArray())
} catch (e: Exception) {
return null
}
return SignalDatabase.callLinks.insertCallLink(
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = CallLinkRoomId.fromCallLinkRootKey(rootKey),
credentials = CallLinkCredentials(callLink.rootKey.toByteArray(), callLink.adminKey?.toByteArray()),
state = SignalCallLinkState(
name = callLink.name,
restrictions = callLink.restrictions.toLocal(),
expiration = Instant.ofEpochMilli(callLink.expirationMs)
),
deletionTimestamp = 0L
)
)
}
}
private fun CallLink.Restrictions.toLocal(): CallLinkState.Restrictions {
return when (this) {
CallLink.Restrictions.ADMIN_APPROVAL -> CallLinkState.Restrictions.ADMIN_APPROVAL
CallLink.Restrictions.NONE -> CallLinkState.Restrictions.NONE
CallLink.Restrictions.UNKNOWN -> CallLinkState.Restrictions.UNKNOWN
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.importer
import androidx.core.content.contentValuesOf
import org.signal.core.util.SqlUtil
import org.signal.core.util.insertInto
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.database.restoreWallpaperAttachment
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.util.parseChatWallpaper
import org.thoughtcrime.securesms.backup.v2.util.toLocal
import org.thoughtcrime.securesms.backup.v2.util.toLocalAttachment
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.recipients.RecipientId
import org.thoughtcrime.securesms.wallpaper.UriChatWallpaper
/**
* Handles the importing of [Chat] models into the local database.
*/
object ChatArchiveImporter {
fun import(chat: Chat, recipientId: RecipientId, importState: ImportState): Long {
val chatColor = chat.style?.toLocal(importState)
val wallpaperAttachmentId: AttachmentId? = chat.style?.wallpaperPhoto?.let { filePointer ->
filePointer.toLocalAttachment(importState)?.let {
SignalDatabase.attachments.restoreWallpaperAttachment(it)
}
}
val chatWallpaper = chat.style?.parseChatWallpaper(wallpaperAttachmentId)
val threadId = SignalDatabase.writableDatabase
.insertInto(ThreadTable.TABLE_NAME)
.values(
ThreadTable.RECIPIENT_ID to recipientId.serialize(),
ThreadTable.PINNED to chat.pinnedOrder,
ThreadTable.ARCHIVED to chat.archived.toInt(),
ThreadTable.READ to if (chat.markedUnread) ThreadTable.ReadStatus.FORCED_UNREAD.serialize() else ThreadTable.ReadStatus.READ.serialize(),
ThreadTable.ACTIVE to 1
)
.run()
SignalDatabase.writableDatabase
.update(
RecipientTable.TABLE_NAME,
contentValuesOf(
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.MESSAGE_EXPIRATION_TIME_VERSION to chat.expireTimerVersion,
RecipientTable.CHAT_COLORS to chatColor?.serialize()?.encode(),
RecipientTable.CUSTOM_CHAT_COLORS_ID to (chatColor?.id ?: ChatColors.Id.NotSet).longValue,
RecipientTable.WALLPAPER_URI to if (chatWallpaper is UriChatWallpaper) chatWallpaper.uri.toString() else null,
RecipientTable.WALLPAPER to chatWallpaper?.serialize()?.encode()
),
"${RecipientTable.ID} = ?",
SqlUtil.buildArgs(recipientId.toLong())
)
return threadId
}
}

View File

@@ -1,9 +1,9 @@
/*
* Copyright 2023 Signal Messenger, LLC
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
package org.thoughtcrime.securesms.backup.v2.importer
import android.content.ContentValues
import androidx.core.content.contentValuesOf
@@ -82,13 +82,13 @@ import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
* An object that will ingest all fo the [ChatItem]s you want to write, buffer them until hitting a specified batch size, and then batch insert them
* for fast throughput.
*/
class ChatItemImportInserter(
class ChatItemArchiveImporter(
private val db: SQLiteDatabase,
private val importState: ImportState,
private val batchSize: Int
) {
companion object {
private val TAG = Log.tag(ChatItemImportInserter::class.java)
private val TAG = Log.tag(ChatItemArchiveImporter::class.java)
private val MESSAGE_COLUMNS = arrayOf(
MessageTable.DATE_SENT,
@@ -150,7 +150,7 @@ class ChatItemImportInserter(
* Indicate that you want to insert the [ChatItem] into the database.
* If this item causes the buffer to hit the batch size, then a batch of items will actually be inserted.
*/
fun insert(chatItem: ChatItem) {
fun import(chatItem: ChatItem) {
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.")

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.importer
import androidx.core.content.contentValuesOf
import org.signal.core.util.Base64
import org.signal.core.util.toInt
import org.signal.core.util.update
import org.thoughtcrime.securesms.backup.v2.proto.Contact
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
/**
* Handles the importing of [Contact] models into the local database.
*/
object ContactArchiveImporter {
fun import(contact: Contact): RecipientId {
val id = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(
aci = ACI.parseOrNull(contact.aci?.toByteArray()),
pni = PNI.parseOrNull(contact.pni?.toByteArray()),
e164 = contact.formattedE164
)
val profileKey = contact.profileKey?.toByteArray()
val values = contentValuesOf(
RecipientTable.BLOCKED to contact.blocked,
RecipientTable.HIDDEN to contact.visibility.toLocal().serialize(),
RecipientTable.TYPE to RecipientTable.RecipientType.INDIVIDUAL.id,
RecipientTable.PROFILE_FAMILY_NAME to contact.profileFamilyName,
RecipientTable.PROFILE_GIVEN_NAME to contact.profileGivenName,
RecipientTable.PROFILE_JOINED_NAME to ProfileName.fromParts(contact.profileGivenName, contact.profileFamilyName).toString(),
RecipientTable.PROFILE_KEY to if (profileKey == null) null else Base64.encodeWithPadding(profileKey),
RecipientTable.PROFILE_SHARING to contact.profileSharing.toInt(),
RecipientTable.USERNAME to contact.username,
RecipientTable.EXTRAS to contact.toLocalExtras().encode()
)
if (contact.registered != null) {
values.put(RecipientTable.UNREGISTERED_TIMESTAMP, 0L)
values.put(RecipientTable.REGISTERED, RecipientTable.RegisteredState.REGISTERED.id)
} else if (contact.notRegistered != null) {
values.put(RecipientTable.UNREGISTERED_TIMESTAMP, contact.notRegistered.unregisteredTimestamp)
values.put(RecipientTable.REGISTERED, RecipientTable.RegisteredState.NOT_REGISTERED.id)
}
SignalDatabase.writableDatabase
.update(RecipientTable.TABLE_NAME)
.values(values)
.where("${RecipientTable.ID} = ?", id)
.run()
return id
}
}
private fun Contact.Visibility.toLocal(): Recipient.HiddenState {
return when (this) {
Contact.Visibility.VISIBLE -> Recipient.HiddenState.NOT_HIDDEN
Contact.Visibility.HIDDEN -> Recipient.HiddenState.HIDDEN
Contact.Visibility.HIDDEN_MESSAGE_REQUEST -> Recipient.HiddenState.HIDDEN_MESSAGE_REQUEST
}
}
private fun Contact.toLocalExtras(): RecipientExtras {
return RecipientExtras(
hideStory = this.hideStory
)
}
private val Contact.formattedE164: String?
get() {
return e164?.let {
PhoneNumberFormatter.get(AppDependencies.application).format(e164.toString())
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.importer
import org.signal.core.util.logging.Log
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.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.util.UuidUtil
/**
* Handles the importing of [DistributionListItem] models into the local database.
*/
object DistributionListArchiveImporter {
private val TAG = Log.tag(DistributionListArchiveImporter.javaClass)
fun import(dlistItem: DistributionListItem, importState: ImportState): RecipientId? {
if (dlistItem.deletionTimestamp != null && dlistItem.deletionTimestamp > 0) {
val dlistId = SignalDatabase.distributionLists.createList(
name = "",
members = emptyList(),
distributionId = DistributionId.from(UuidUtil.fromByteString(dlistItem.distributionId)),
allowsReplies = false,
deletionTimestamp = dlistItem.deletionTimestamp,
storageId = null,
privacyMode = DistributionListPrivacyMode.ONLY_WITH
)!!
return SignalDatabase.distributionLists.getRecipientId(dlistId)!!
}
val dlist = dlistItem.distributionList ?: return null
val members: List<RecipientId> = dlist.memberRecipientIds
.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()}")
}
val distributionId = DistributionId.from(UuidUtil.fromByteString(dlistItem.distributionId))
val privacyMode = dlist.privacyMode.toLocalPrivacyMode()
val dlistId = SignalDatabase.distributionLists.createList(
name = dlist.name,
members = members,
distributionId = distributionId,
allowsReplies = dlist.allowReplies,
deletionTimestamp = dlistItem.deletionTimestamp ?: 0,
storageId = null,
privacyMode = privacyMode
)!!
return SignalDatabase.distributionLists.getRecipientId(dlistId)!!
}
}
private fun DistributionList.PrivacyMode.toLocalPrivacyMode(): DistributionListPrivacyMode {
return when (this) {
DistributionList.PrivacyMode.UNKNOWN -> DistributionListPrivacyMode.ALL
DistributionList.PrivacyMode.ONLY_WITH -> DistributionListPrivacyMode.ONLY_WITH
DistributionList.PrivacyMode.ALL -> DistributionListPrivacyMode.ALL
DistributionList.PrivacyMode.ALL_EXCEPT -> DistributionListPrivacyMode.ALL_EXCEPT
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.importer
import android.content.ContentValues
import org.signal.core.util.Base64
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.groups.GroupSecretParams
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.DecryptedTimer
import org.signal.storageservice.protos.groups.local.EnabledState
import org.thoughtcrime.securesms.backup.v2.ArchiveGroup
import org.thoughtcrime.securesms.backup.v2.proto.Group
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
import org.whispersystems.signalservice.api.push.ServiceId
/**
* Handles the importing of [ArchiveGroup] models into the local database.
*/
object GroupArchiveImporter {
fun import(group: ArchiveGroup): RecipientId {
val masterKey = GroupMasterKey(group.masterKey.toByteArray())
val groupId = GroupId.v2(masterKey)
val operations = AppDependencies.groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(masterKey))
val decryptedState = if (group.snapshot == null) {
DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
} else {
group.snapshot.toLocal(operations)
}
val values = ContentValues().apply {
put(RecipientTable.GROUP_ID, groupId.toString())
put(RecipientTable.AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize())
put(RecipientTable.PROFILE_SHARING, group.whitelisted)
put(RecipientTable.TYPE, RecipientTable.RecipientType.GV2.id)
put(RecipientTable.STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
if (group.hideStory) {
val extras = RecipientExtras.Builder().hideStory(true).build()
put(RecipientTable.EXTRAS, extras.encode())
}
}
val recipientId = SignalDatabase.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.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 Group.AccessControl.AccessRequired.toLocal(): AccessControl.AccessRequired {
return when (this) {
Group.AccessControl.AccessRequired.UNKNOWN -> AccessControl.AccessRequired.UNKNOWN
Group.AccessControl.AccessRequired.ANY -> AccessControl.AccessRequired.ANY
Group.AccessControl.AccessRequired.MEMBER -> AccessControl.AccessRequired.MEMBER
Group.AccessControl.AccessRequired.ADMINISTRATOR -> AccessControl.AccessRequired.ADMINISTRATOR
Group.AccessControl.AccessRequired.UNSATISFIABLE -> AccessControl.AccessRequired.UNSATISFIABLE
}
}
private fun Group.AccessControl.toLocal(): AccessControl {
return AccessControl(members = this.members.toLocal(), attributes = this.attributes.toLocal(), addFromInviteLink = this.addFromInviteLink.toLocal())
}
private fun Group.Member.Role.toLocal(): Member.Role {
return when (this) {
Group.Member.Role.UNKNOWN -> Member.Role.UNKNOWN
Group.Member.Role.DEFAULT -> Member.Role.DEFAULT
Group.Member.Role.ADMINISTRATOR -> Member.Role.ADMINISTRATOR
}
}
private fun Group.Member.toLocal(): DecryptedMember {
return DecryptedMember(aciBytes = userId, role = role.toLocal(), joinedAtRevision = joinedAtVersion)
}
private fun Group.MemberPendingAdminApproval.toLocal(): DecryptedRequestingMember {
return DecryptedRequestingMember(
aciBytes = this.userId,
timestamp = this.timestamp
)
}
private fun Group.MemberBanned.toLocal(): DecryptedBannedMember {
return DecryptedBannedMember(
serviceIdBytes = this.userId,
timestamp = this.timestamp
)
}
private fun Group.GroupSnapshot.toLocal(operations: GroupsV2Operations.GroupOperations): DecryptedGroup {
return DecryptedGroup(
title = this.title?.title ?: "",
avatar = this.avatarUrl,
disappearingMessagesTimer = DecryptedTimer(duration = this.disappearingMessagesTimer?.disappearingMessagesDuration ?: 0),
accessControl = this.accessControl?.toLocal(),
revision = this.version,
members = this.members.map { member -> member.toLocal() },
pendingMembers = this.membersPendingProfileKey.map { pending -> pending.toLocal(operations) },
requestingMembers = this.membersPendingAdminApproval.map { requesting -> requesting.toLocal() },
inviteLinkPassword = this.inviteLinkPassword,
description = this.description?.descriptionText ?: "",
isAnnouncementGroup = if (this.announcements_only) EnabledState.ENABLED else EnabledState.DISABLED,
bannedMembers = this.members_banned.map { it.toLocal() }
)
}

View File

@@ -44,9 +44,9 @@ import java.util.Currency
/**
* Handles importing/exporting [AccountData] frames for an archive.
*/
object AccountDataBackupProcessor {
object AccountDataArchiveProcessor {
private val TAG = Log.tag(AccountDataBackupProcessor::class)
private val TAG = Log.tag(AccountDataArchiveProcessor::class)
fun export(db: SignalDatabase, signalStore: SignalStore, emitter: BackupFrameEmitter) {
val context = AppDependencies.application

View File

@@ -8,7 +8,7 @@ package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
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.importer.AdHodCallArchiveImporter
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
@@ -17,9 +17,9 @@ import org.thoughtcrime.securesms.database.SignalDatabase
/**
* Handles importing/exporting [AdHocCall] frames for an archive.
*/
object AdHocCallBackupProcessor {
object AdHocCallArchiveProcessor {
val TAG = Log.tag(AdHocCallBackupProcessor::class.java)
val TAG = Log.tag(AdHocCallArchiveProcessor::class.java)
fun export(db: SignalDatabase, emitter: BackupFrameEmitter) {
db.callTable.getAdhocCallsForBackup().use { reader ->
@@ -30,6 +30,6 @@ object AdHocCallBackupProcessor {
}
fun import(call: AdHocCall, importState: ImportState) {
SignalDatabase.calls.restoreCallLogFromBackup(call, importState)
AdHodCallArchiveImporter.import(call, importState)
}
}

View File

@@ -9,7 +9,7 @@ import org.signal.core.util.logging.Log
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.importer.ChatArchiveImporter
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
@@ -19,8 +19,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Handles importing/exporting [Chat] frames for an archive.
*/
object ChatBackupProcessor {
val TAG = Log.tag(ChatBackupProcessor::class.java)
object ChatArchiveProcessor {
val TAG = Log.tag(ChatArchiveProcessor::class.java)
fun export(db: SignalDatabase, exportState: ExportState, emitter: BackupFrameEmitter) {
db.threadTable.getThreadsForBackup(db).use { reader ->
@@ -42,10 +42,9 @@ object ChatBackupProcessor {
return
}
SignalDatabase.threads.restoreFromBackup(chat, recipientId, importState).let { threadId ->
importState.chatIdToLocalRecipientId[chat.id] = recipientId
importState.chatIdToLocalThreadId[chat.id] = threadId
importState.chatIdToBackupRecipientId[chat.id] = chat.recipientId
}
val threadId = ChatArchiveImporter.import(chat, recipientId, importState)
importState.chatIdToLocalRecipientId[chat.id] = recipientId
importState.chatIdToLocalThreadId[chat.id] = threadId
importState.chatIdToBackupRecipientId[chat.id] = chat.recipientId
}
}

View File

@@ -8,9 +8,9 @@ package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
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
import org.thoughtcrime.securesms.backup.v2.importer.ChatItemArchiveImporter
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
@@ -19,8 +19,8 @@ import org.thoughtcrime.securesms.database.SignalDatabase
/**
* Handles importing/exporting [ChatItem] frames for an archive.
*/
object ChatItemBackupProcessor {
val TAG = Log.tag(ChatItemBackupProcessor::class.java)
object ChatItemArchiveProcessor {
val TAG = Log.tag(ChatItemArchiveProcessor::class.java)
fun export(db: SignalDatabase, exportState: ExportState, emitter: BackupFrameEmitter) {
db.messageTable.getMessagesForBackup(db, exportState.backupTime, exportState.mediaBackupEnabled).use { chatItems ->
@@ -35,7 +35,7 @@ object ChatItemBackupProcessor {
}
}
fun beginImport(importState: ImportState): ChatItemImportInserter {
fun beginImport(importState: ImportState): ChatItemArchiveImporter {
return SignalDatabase.messages.createChatItemInserter(importState)
}
}

View File

@@ -13,10 +13,11 @@ import org.thoughtcrime.securesms.backup.v2.database.getAllForBackup
import org.thoughtcrime.securesms.backup.v2.database.getCallLinksForBackup
import org.thoughtcrime.securesms.backup.v2.database.getContactsForBackup
import org.thoughtcrime.securesms.backup.v2.database.getGroupsForBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreContactFromBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreFromBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreGroupFromBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreReleaseNotes
import org.thoughtcrime.securesms.backup.v2.importer.CallLinkArchiveImporter
import org.thoughtcrime.securesms.backup.v2.importer.ContactArchiveImporter
import org.thoughtcrime.securesms.backup.v2.importer.DistributionListArchiveImporter
import org.thoughtcrime.securesms.backup.v2.importer.GroupArchiveImporter
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.proto.ReleaseNotes
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
@@ -27,9 +28,9 @@ import org.thoughtcrime.securesms.recipients.Recipient
/**
* Handles importing/exporting [ArchiveRecipient] frames for an archive.
*/
object RecipientBackupProcessor {
object RecipientArchiveProcessor {
val TAG = Log.tag(RecipientBackupProcessor::class.java)
val TAG = Log.tag(RecipientArchiveProcessor::class.java)
fun export(db: SignalDatabase, signalStore: SignalStore, exportState: ExportState, emitter: BackupFrameEmitter) {
val selfId = db.recipientTable.getByAci(signalStore.accountValues.aci!!).get().toLong()
@@ -79,12 +80,12 @@ object RecipientBackupProcessor {
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)
recipient.distributionList != null -> SignalDatabase.distributionLists.restoreFromBackup(recipient.distributionList, importState)
recipient.contact != null -> ContactArchiveImporter.import(recipient.contact)
recipient.group != null -> GroupArchiveImporter.import(recipient.group)
recipient.distributionList != null -> DistributionListArchiveImporter.import(recipient.distributionList, importState)
recipient.self != null -> Recipient.self().id
recipient.releaseNotes != null -> SignalDatabase.recipients.restoreReleaseNotes()
recipient.callLink != null -> SignalDatabase.callLinks.restoreFromBackup(recipient.callLink)
recipient.callLink != null -> CallLinkArchiveImporter.import(recipient.callLink)
else -> {
Log.w(TAG, "Unrecognized recipient type!")
null

View File

@@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob
/**
* Handles importing/exporting [StickerPack] frames for an archive.
*/
object StickerBackupProcessor {
object StickerArchiveProcessor {
fun export(db: SignalDatabase, emitter: BackupFrameEmitter) {
StickerPackRecordReader(db.stickerTable.allStickerPacks).use { reader ->
var record: StickerPackRecord? = reader.next

View File

@@ -10,8 +10,6 @@ import android.content.Intent
import android.widget.Toast
import androidx.core.app.ShareCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.CallLinks
@@ -27,7 +25,7 @@ import org.thoughtcrime.securesms.events.CallParticipant
*/
class CallInfoCallbacks(
private val activity: BaseActivity,
private val controlsAndInfoViewModel: ControlsAndInfoViewModel,
private val controlsAndInfoViewModel: ControlsAndInfoViewModel
) : CallInfoView.Callbacks {
override fun onShareLinkClicked() {

View File

@@ -247,6 +247,14 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val rawDatabase: net.zetetic.database.sqlcipher.SQLiteDatabase
get() = instance!!.rawWritableDatabase
@JvmStatic
val readableDatabase: SQLiteDatabase
get() = instance!!.signalReadableDatabase
@JvmStatic
val writableDatabase: SQLiteDatabase
get() = instance!!.signalWritableDatabase
@JvmStatic
val backupDatabase: net.zetetic.database.sqlcipher.SQLiteDatabase
get() = instance!!.rawReadableDatabase