mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Refactor archive importing.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user