TM-SGNL-iOS/SignalServiceKit/MessageBackup/Archivers/Recipient/MessageBackupContactRecipientArchiver.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

708 lines
29 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import GRDB
import LibSignalClient
/// Archives ``SignalRecipient``s as ``BackupProto_Contact`` recipients.
public class MessageBackupContactRecipientArchiver: MessageBackupProtoArchiver {
typealias RecipientId = MessageBackup.RecipientId
typealias RecipientAppId = MessageBackup.RecipientArchivingContext.Address
typealias ArchiveMultiFrameResult = MessageBackup.ArchiveMultiFrameResult<RecipientAppId>
private typealias ArchiveFrameError = MessageBackup.ArchiveFrameError<RecipientAppId>
typealias RestoreFrameResult = MessageBackup.RestoreFrameResult<RecipientId>
private typealias RestoreFrameError = MessageBackup.RestoreFrameError<RecipientId>
private let avatarFetcher: MessageBackupAvatarFetcher
private let blockingManager: MessageBackup.Shims.BlockingManager
private let dateProvider: DateProvider
private let profileManager: MessageBackup.Shims.ProfileManager
private let recipientHidingManager: RecipientHidingManager
private let recipientManager: any SignalRecipientManager
private let recipientStore: MessageBackupRecipientStore
private let signalServiceAddressCache: SignalServiceAddressCache
private let storyStore: MessageBackupStoryStore
private let threadStore: MessageBackupThreadStore
private let tsAccountManager: TSAccountManager
private let usernameLookupManager: UsernameLookupManager
public init(
avatarFetcher: MessageBackupAvatarFetcher,
blockingManager: MessageBackup.Shims.BlockingManager,
dateProvider: @escaping DateProvider,
profileManager: MessageBackup.Shims.ProfileManager,
recipientHidingManager: RecipientHidingManager,
recipientManager: any SignalRecipientManager,
recipientStore: MessageBackupRecipientStore,
signalServiceAddressCache: SignalServiceAddressCache,
storyStore: MessageBackupStoryStore,
threadStore: MessageBackupThreadStore,
tsAccountManager: TSAccountManager,
usernameLookupManager: UsernameLookupManager
) {
self.avatarFetcher = avatarFetcher
self.blockingManager = blockingManager
self.dateProvider = dateProvider
self.profileManager = profileManager
self.recipientHidingManager = recipientHidingManager
self.recipientManager = recipientManager
self.recipientStore = recipientStore
self.signalServiceAddressCache = signalServiceAddressCache
self.storyStore = storyStore
self.threadStore = threadStore
self.tsAccountManager = tsAccountManager
self.usernameLookupManager = usernameLookupManager
}
// MARK: -
func archiveAllContactRecipients(
stream: MessageBackupProtoOutputStream,
context: MessageBackup.RecipientArchivingContext
) throws(CancellationError) -> ArchiveMultiFrameResult {
let whitelistedAddresses = Set(profileManager.allWhitelistedAddresses(tx: context.tx))
let blockedAddresses = blockingManager.blockedAddresses(tx: context.tx)
var errors = [ArchiveFrameError]()
func writeToStream(
contact: BackupProto_Contact,
contactAddress: MessageBackup.ContactAddress
) {
let maybeError: ArchiveFrameError? = Self.writeFrameToStream(
stream,
objectId: .contact(contactAddress),
frameBuilder: {
let recipientAddress = contactAddress.asArchivingAddress()
let recipientId = context.assignRecipientId(to: recipientAddress)
var recipient = BackupProto_Recipient()
recipient.id = recipientId.value
recipient.destination = .contact(contact)
var frame = BackupProto_Frame()
frame.item = .recipient(recipient)
return frame
}
)
if let maybeError {
errors.append(maybeError)
}
}
/// Track all the `ServiceId`s that we've archived, so we don't attempt
/// to archive a `Contact` frame twice for the same service ID.
var archivedServiceIds = Set<ServiceId>()
/// Track all the phone numbers we've archived too, so we don't attempt to archive a
/// `Contact` twice for the same e164.
var archivedPhoneNumbers = Set<String>()
/// First, we enumerate all `SignalRecipient`s, which are our "primary
/// key" for contacts. They directly contain many of the fields we store
/// in a `Contact` recipient, with the other fields keyed off data in
/// the recipient.
let recipientBlock: (SignalRecipient) -> Void = { recipient in
guard
let contactAddress = MessageBackup.ContactAddress(
aci: recipient.aci,
pni: recipient.pni,
e164: E164(recipient.phoneNumber?.stringValue)
)
else {
/// Skip recipients with no identifiers, but don't add to the
/// list of errors.
Logger.warn("Skipping empty SignalRecipient!")
return
}
guard !context.localIdentifiers.containsAnyOf(
aci: contactAddress.aci,
phoneNumber: contactAddress.e164,
pni: contactAddress.pni
) else {
// Skip the local user.
return
}
/// Track the `ServiceId`s for this `SignalRecipient`, so we don't
/// later try and create a duplicate `Contact` frame.
if let aci = contactAddress.aci {
archivedServiceIds.insert(aci)
}
if let pni = contactAddress.pni {
archivedServiceIds.insert(pni)
}
if let e164 = contactAddress.e164 {
archivedPhoneNumbers.insert(e164.stringValue)
}
var isStoryHidden = false
if let aci = recipient.aci {
do {
isStoryHidden = try self.storyStore.getOrCreateStoryContextAssociatedData(
for: aci,
context: context
).isHidden
} catch let error {
errors.append(.archiveFrameError(
.unableToReadStoryContextAssociatedData(error),
.contact(contactAddress)
))
}
}
let identity: OWSRecipientIdentity?
do {
// Read directly from the OWSRecipientIdentity table, bypassing
// OWSIdentityManager, as we already are working directly
// with the SignalRecipient and don't need serviceId-based checks.
identity = try RecipientIdentityRecord
.filter(Column(RecipientIdentityRecord.CodingKeys.uniqueId) == recipient.uniqueId)
.fetchOne(context.tx.databaseConnection)
.map { try OWSRecipientIdentity.fromRecord($0) }
} catch let error {
errors.append(.archiveFrameError(
.unableToFetchRecipientIdentity(error),
.contact(contactAddress)
))
return
}
let contact = self.buildContactRecipient(
aci: contactAddress.aci,
pni: contactAddress.pni,
e164: contactAddress.e164,
username: recipient.aci.flatMap { aci in
self.usernameLookupManager.fetchUsername(
forAci: aci,
transaction: context.tx
)
},
isBlocked: blockedAddresses.contains(recipient.address),
isWhitelisted: whitelistedAddresses.contains(recipient.address),
isStoryHidden: isStoryHidden,
visibility: { () -> BackupProto_Contact.Visibility in
guard let hiddenRecipient = self.recipientHidingManager.fetchHiddenRecipient(
signalRecipient: recipient,
tx: context.tx
) else {
return .visible
}
if
self.recipientHidingManager.isHiddenRecipientThreadInMessageRequest(
hiddenRecipient: hiddenRecipient,
contactThread: self.threadStore.fetchContactThread(
recipient: recipient,
context: context
),
tx: context.tx
)
{
return .hiddenMessageRequest
} else {
return .hidden
}
}(),
registration: { () -> BackupProto_Contact.OneOf_Registration in
if !recipient.isRegistered {
var notRegistered = BackupProto_Contact.NotRegistered()
notRegistered.unregisteredTimestamp = recipient.unregisteredAtTimestamp ?? SignalRecipient.Constants.distantPastUnregisteredTimestamp
return .notRegistered(notRegistered)
}
return .registered(BackupProto_Contact.Registered())
}(),
userProfile: self.profileManager.getUserProfile(
for: recipient.address,
tx: context.tx
),
identity: identity
)
writeToStream(contact: contact, contactAddress: contactAddress)
}
do {
try recipientStore.enumerateAllSignalRecipients(context, block: { recipient in
autoreleasepool { recipientBlock(recipient) }
})
} catch let error as CancellationError {
throw error
} catch {
return .completeFailure(.fatalArchiveError(.recipientIteratorError(error)))
}
/// After enumerating all `SignalRecipient`s, we enumerate
/// `OWSUserProfile`s. It's possible that we'll have an `OWSUserProfile`
/// for a user for whom we have no `SignalRecipient`; for example, a
/// member of a group we're in whose profile we've fetched, but with
/// whom we've never messaged.
///
/// It's important that the profile info we have for those users is
/// included in the Backup. However, if we had a `SignalRecipient` for
/// the profile (both tables store an ACI), the profile info was already
/// archived and we should not make another `Contact` frame for the same
/// ACI.
///
/// A known side-effect of archiving `Contact` frames for
/// `OWSUserProfile`s is that when we restore these frames we'll create
/// both an `OWSUserProfile` and a `SignalRecipient` for this entry.
/// That's fine, even good: ideally we want to move towards a 1:1
/// relationship between `SignalRecipient` and other user-related models
/// like `OWSUserProfile`. If, in the future, we have an enforced 1:1
/// relationship between `SignalRecipient` and `OWSUserProfile`, we can
/// remove this code.
profileManager.enumerateUserProfiles(tx: context.tx) { userProfile in
autoreleasepool {
if let serviceId = userProfile.serviceId {
let (inserted, _) = archivedServiceIds.insert(serviceId)
if !inserted {
/// Bail early if we've already archived a `Contact` for this
/// service ID.
return
}
}
if let phoneNumber = userProfile.phoneNumber {
let (inserted, _) = archivedPhoneNumbers.insert(phoneNumber)
if !inserted {
/// Bail early if we've already archived a `Contact` for this
/// phone number.
return
}
}
guard
let contactAddress = MessageBackup.ContactAddress(
aci: userProfile.serviceId as? Aci,
pni: userProfile.serviceId as? Pni,
e164: userProfile.phoneNumber.flatMap { E164($0) }
)
else {
/// Skip profiles with no identifiers, but don't add to the
/// list of errors.
Logger.warn("Skipping empty OWSUserProfile!")
return
}
let signalServiceAddress: MessageBackup.InteropAddress
switch userProfile.internalAddress {
case .localUser:
/// Skip the local user. We need to check `internalAddress`
/// here, since the "local user profile" has historically been
/// persisted with a special, magic phone number.
return
case .otherUser(let _signalServiceAddress):
signalServiceAddress = _signalServiceAddress
}
let contact = buildContactRecipient(
aci: contactAddress.aci,
pni: contactAddress.pni,
e164: contactAddress.e164,
username: nil, // If we have a user profile, we have no username.
isBlocked: blockedAddresses.contains(signalServiceAddress),
isWhitelisted: whitelistedAddresses.contains(signalServiceAddress),
isStoryHidden: false, // Can't have a story if there's no recipient.
visibility: .visible, // Can't have hidden if there's no recipient.
registration: {
// We don't know if they're registered; if we did, we'd have
// a recipient.
var notRegistered = BackupProto_Contact.NotRegistered()
notRegistered.unregisteredTimestamp = 0
return .notRegistered(notRegistered)
}(),
userProfile: userProfile,
// We don't have (and can't fetch) identity info for
// profile addresses without SignalRecipients.
identity: nil
)
writeToStream(
contact: contact,
contactAddress: contactAddress
)
}
}
if errors.isEmpty {
return .success
} else {
return .partialSuccess(errors)
}
}
/// It is possible to have a TSContactThread for which we have no SignalRecipient
/// or OWSUserProfile. One way to create this is to tap "Call with Signal" from the system
/// contacts app, for a number that is not registered on Signal.
/// If this happens, when we archive the TSContactThread we need to also archive
/// a Contact recipient that we create on-the-fly. This is only used if we were unable
/// to find a Recipient for the thread's address; in other words there was not a
/// corresponding recipient that we archived earlier.
func archiveContactRecipientForOrphanedContactThread(
_ contactThread: TSContactThread,
address: MessageBackup.ContactAddress,
stream: MessageBackupProtoOutputStream,
context: MessageBackup.ChatArchivingContext
) -> MessageBackup.ArchiveSingleFrameResult<RecipientId, MessageBackup.ThreadUniqueId> {
let existingRecipient = recipientStore.fetchRecipient(
for: address,
context: context.recipientContext
)
// If we have an existing recipient, this is an error. It means we
// _should_ have found the recipient on the context, but did not.
guard existingRecipient == nil else {
return .failure(.archiveFrameError(
.referencedRecipientIdMissing(address.asArchivingAddress()),
.init(thread: contactThread)
))
}
// We don't know if they're registered; if we did there
// would be a SignalRecipient.
var registration = BackupProto_Contact.NotRegistered()
registration.unregisteredTimestamp = 0
let contactProto = buildContactRecipient(
aci: address.aci,
pni: address.pni,
e164: address.e164,
username: nil,
isBlocked: blockingManager.blockedAddresses(tx: context.tx)
.contains(address.asInteropAddress()),
isWhitelisted: profileManager.allWhitelistedAddresses(tx: context.tx)
.contains(address.asInteropAddress()),
// If there's no recipient, neither can be hidden
isStoryHidden: false,
visibility: .visible,
registration: .notRegistered(registration),
userProfile: nil,
identity: nil
)
let recipientAddress = address.asArchivingAddress()
let recipientId = context.recipientContext.assignRecipientId(to: recipientAddress)
let maybeError: MessageBackup.ArchiveFrameError<MessageBackup.ThreadUniqueId>?
maybeError = Self.writeFrameToStream(
stream,
objectId: .init(thread: contactThread),
frameBuilder: {
var recipient = BackupProto_Recipient()
recipient.id = recipientId.value
recipient.destination = .contact(contactProto)
var frame = BackupProto_Frame()
frame.item = .recipient(recipient)
return frame
}
)
if let maybeError {
return .failure(maybeError)
}
return .success(recipientId)
}
private func buildContactRecipient(
aci: Aci?,
pni: Pni?,
e164: E164?,
username: String?,
isBlocked: Bool,
isWhitelisted: Bool,
isStoryHidden: Bool,
visibility: BackupProto_Contact.Visibility,
registration: BackupProto_Contact.OneOf_Registration,
userProfile: OWSUserProfile?,
identity: OWSRecipientIdentity?
) -> BackupProto_Contact {
var contact = BackupProto_Contact()
contact.blocked = isBlocked
contact.profileSharing = isWhitelisted
contact.hideStory = isStoryHidden
contact.visibility = visibility
contact.registration = registration
if let identity, let identityKey = try? identity.identityKeyObject {
// `serialize()`, which includes the keyType prefix.
contact.identityKey = Data(identityKey.publicKey.serialize())
switch identity.verificationState {
case .default, .defaultAcknowledged:
contact.identityState = .default
case .verified:
contact.identityState = .verified
case .noLongerVerified:
contact.identityState = .unverified
}
}
if let aci {
contact.aci = aci.rawUUID.data
}
if let pni {
contact.pni = pni.rawUUID.data
}
if let e164UInt = e164?.uint64Value {
contact.e164 = e164UInt
}
if let username {
contact.username = username
}
if let profileKey = userProfile?.profileKey {
contact.profileKey = profileKey.keyData
}
if let givenName = userProfile?.givenName?.nilIfEmpty {
contact.profileGivenName = givenName
}
if let familyName = userProfile?.familyName?.nilIfEmpty {
contact.profileFamilyName = familyName
}
return contact
}
// MARK: -
func restoreContactRecipientProto(
_ contactProto: BackupProto_Contact,
recipient: BackupProto_Recipient,
context: MessageBackup.RecipientRestoringContext
) -> RestoreFrameResult {
func restoreFrameError(
_ error: RestoreFrameError.ErrorType,
line: UInt = #line
) -> RestoreFrameResult {
return .failure([.restoreFrameError(error, recipient.recipientId, line: line)])
}
let isRegistered: Bool
let unregisteredTimestamp: UInt64?
switch contactProto.registration {
case nil:
return .failure([.restoreFrameError(
.invalidProtoData(.contactWithoutRegistrationInfo),
recipient.recipientId
)])
case .notRegistered(let notRegisteredProto):
isRegistered = false
unregisteredTimestamp = notRegisteredProto.unregisteredTimestamp
case .registered:
isRegistered = true
unregisteredTimestamp = nil
}
let aci: Aci?
let pni: Pni?
let e164: E164?
let profileKey: Aes256Key?
if contactProto.hasAci {
guard let aciUuid = UUID(data: contactProto.aci) else {
return restoreFrameError(.invalidProtoData(.invalidAci(protoClass: BackupProto_Contact.self)))
}
aci = Aci.init(fromUUID: aciUuid)
} else {
aci = nil
}
if contactProto.hasPni {
guard let pniUuid = UUID(data: contactProto.pni) else {
return restoreFrameError(.invalidProtoData(.invalidPni(protoClass: BackupProto_Contact.self)))
}
pni = Pni.init(fromUUID: pniUuid)
} else {
pni = nil
}
if contactProto.hasE164 {
guard let protoE164 = E164(contactProto.e164) else {
return restoreFrameError(.invalidProtoData(.invalidE164(protoClass: BackupProto_Contact.self)))
}
e164 = protoE164
} else {
e164 = nil
}
if contactProto.hasProfileKey {
guard let protoProfileKey = Aes256Key(data: contactProto.profileKey) else {
return restoreFrameError(.invalidProtoData(.invalidProfileKey(protoClass: BackupProto_Contact.self)))
}
profileKey = protoProfileKey
} else {
profileKey = nil
}
/// This check will fail if all these identifiers are `nil`.
guard let backupContactAddress = MessageBackup.ContactAddress(
aci: aci,
pni: pni,
e164: e164
) else {
return restoreFrameError(.invalidProtoData(.contactWithoutIdentifiers))
}
context[recipient.recipientId] = .contact(backupContactAddress)
// Stop early if this is the local user. That shouldn't happen.
let profileInsertableAddress: OWSUserProfile.InsertableAddress
if let serviceId = backupContactAddress.aci ?? backupContactAddress.pni {
profileInsertableAddress = OWSUserProfile.insertableAddress(
serviceId: serviceId,
localIdentifiers: context.localIdentifiers
)
} else if let phoneNumber = backupContactAddress.e164 {
profileInsertableAddress = OWSUserProfile.insertableAddress(
legacyPhoneNumberFromBackupRestore: phoneNumber,
localIdentifiers: context.localIdentifiers
)
} else {
return restoreFrameError(.developerError(OWSAssertionError("How did we have no identifiers after constructing a backup contact address?")))
}
switch profileInsertableAddress {
case .localUser:
return restoreFrameError(.invalidProtoData(.otherContactWithLocalIdentifiers))
case .otherUser, .legacyUserPhoneNumberFromBackupRestore:
break
}
let recipientProto = recipient
let recipient: SignalRecipient = .fromBackup(
backupContactAddress,
isRegistered: isRegistered,
unregisteredAtTimestamp: unregisteredTimestamp
)
do {
try recipientStore.insertRecipient(recipient, context: context)
} catch {
return .failure([.restoreFrameError(.databaseInsertionFailed(error), recipientProto.recipientId)])
}
/// No Backup code should be relying on the SSA cache, but once we've
/// finished restoring and launched we want the cache to have accurate
/// mappings based on the recipients we just restored.
signalServiceAddressCache.updateRecipient(recipient, tx: context.tx)
if
let aci = recipient.aci,
contactProto.hasUsername
{
usernameLookupManager.saveUsername(contactProto.username, forAci: aci, transaction: context.tx)
}
if contactProto.hasIdentityKey {
let identityKey: Data
do {
identityKey = try IdentityKey(publicKey: PublicKey(contactProto.identityKey))
// 'keyBytes', which drops the keyType prefix
.publicKey.keyBytes.asData
} catch {
return .failure([.restoreFrameError(.invalidProtoData(.invalidContactIdentityKey), recipientProto.recipientId)])
}
let verificationState: OWSVerificationState
switch contactProto.identityState {
case .default:
verificationState = .default
case .verified:
verificationState = .verified
case .unverified:
verificationState = .noLongerVerified
case .UNRECOGNIZED:
return .failure([.restoreFrameError(
.invalidProtoData(.unknownContactIdentityState),
recipientProto.recipientId
)])
}
// Write directly to the OWSRecipientIdentity table, bypassing
// OWSIdentityManager, as we already are working directly
// with the SignalRecipient and don't need serviceId-based checks.
let identity = OWSRecipientIdentity(
recipientUniqueId: recipient.uniqueId,
identityKey: identityKey,
isFirstKnownKey: true,
createdAt: dateProvider(),
verificationState: verificationState
)
do {
try identity.asRecord().insert(context.tx.databaseConnection)
} catch {
return .failure([.restoreFrameError(
.databaseInsertionFailed(error),
recipientProto.recipientId
)])
}
}
if contactProto.profileSharing {
// Add to the whitelist.
profileManager.addToWhitelist(recipient.address, tx: context.tx)
}
if contactProto.blocked {
blockingManager.addBlockedAddress(recipient.address, tx: context.tx)
}
do {
func addHiddenRecipient(isHiddenInKnownMessageRequestState: Bool) throws {
try recipientHidingManager.addHiddenRecipient(
recipient,
inKnownMessageRequestState: isHiddenInKnownMessageRequestState,
wasLocallyInitiated: false,
tx: context.tx
)
context.setNeedsPostRestoreContactHiddenInfoMessage(
recipientId: recipientProto.recipientId
)
}
switch contactProto.visibility {
case .hidden:
try addHiddenRecipient(isHiddenInKnownMessageRequestState: false)
case .hiddenMessageRequest:
try addHiddenRecipient(isHiddenInKnownMessageRequestState: true)
case .visible, .UNRECOGNIZED:
break
}
} catch let error {
return restoreFrameError(.databaseInsertionFailed(error))
}
var partialErrors = [MessageBackup.RestoreFrameError<RecipientId>]()
// We only need to active hide, since unhidden is the default.
if contactProto.hideStory, let aci = backupContactAddress.aci {
do {
try storyStore.createStoryContextAssociatedData(
for: aci,
isHidden: true,
context: context
)
} catch let error {
// Don't fail entirely; the story will just be unhidden.
partialErrors.append(.restoreFrameError(.databaseInsertionFailed(error), recipientProto.recipientId))
}
}
profileManager.upsertOtherUserProfile(
insertableAddress: profileInsertableAddress,
givenName: contactProto.profileGivenName.nilIfEmpty,
familyName: contactProto.profileFamilyName.nilIfEmpty,
profileKey: profileKey,
tx: context.tx
)
if partialErrors.isEmpty {
return .success
} else {
return .partialRestore(partialErrors)
}
}
}