Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Backups/Archiving/Archivers/Recipient/BackupArchiveContactRecipientArchiver.swift
1 views
//
// 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 BackupArchiveContactRecipientArchiver: BackupArchiveProtoStreamWriter {
    typealias RecipientId = BackupArchive.RecipientId
    typealias RecipientAppId = BackupArchive.RecipientArchivingContext.Address

    typealias ArchiveMultiFrameResult = BackupArchive.ArchiveMultiFrameResult<RecipientAppId>
    private typealias ArchiveFrameError = BackupArchive.ArchiveFrameError<RecipientAppId>

    typealias RestoreFrameResult = BackupArchive.RestoreFrameResult<RecipientId>
    private typealias RestoreFrameError = BackupArchive.RestoreFrameError<RecipientId>

    private let avatarDefaultColorManager: AvatarDefaultColorManager
    private let avatarFetcher: BackupArchiveAvatarFetcher
    private let blockingManager: BackupArchive.Shims.BlockingManager
    private let contactManager: BackupArchive.Shims.ContactManager
    private let keyTransparencyStore: KeyTransparencyStore
    private let nicknameManager: NicknameManager
    private let profileManager: BackupArchive.Shims.ProfileManager
    private let recipientHidingManager: RecipientHidingManager
    private let recipientManager: any SignalRecipientManager
    private let recipientStore: BackupArchiveRecipientStore
    private let signalServiceAddressCache: SignalServiceAddressCache
    private let storyStore: BackupArchiveStoryStore
    private let threadStore: BackupArchiveThreadStore
    private let tsAccountManager: TSAccountManager
    private let usernameLookupManager: UsernameLookupManager

    public init(
        avatarDefaultColorManager: AvatarDefaultColorManager,
        avatarFetcher: BackupArchiveAvatarFetcher,
        blockingManager: BackupArchive.Shims.BlockingManager,
        contactManager: BackupArchive.Shims.ContactManager,
        keyTransparencyStore: KeyTransparencyStore,
        nicknameManager: NicknameManager,
        profileManager: BackupArchive.Shims.ProfileManager,
        recipientHidingManager: RecipientHidingManager,
        recipientManager: any SignalRecipientManager,
        recipientStore: BackupArchiveRecipientStore,
        signalServiceAddressCache: SignalServiceAddressCache,
        storyStore: BackupArchiveStoryStore,
        threadStore: BackupArchiveThreadStore,
        tsAccountManager: TSAccountManager,
        usernameLookupManager: UsernameLookupManager,
    ) {
        self.avatarDefaultColorManager = avatarDefaultColorManager
        self.avatarFetcher = avatarFetcher
        self.blockingManager = blockingManager
        self.contactManager = contactManager
        self.keyTransparencyStore = keyTransparencyStore
        self.nicknameManager = nicknameManager
        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: BackupArchiveProtoOutputStream,
        context: BackupArchive.RecipientArchivingContext,
    ) throws(CancellationError) -> ArchiveMultiFrameResult {
        let whitelistedAddresses = Set(profileManager.allWhitelistedAddresses(tx: context.tx))

        let blockedRecipientIds = blockingManager.blockedRecipientIds(tx: context.tx)

        var errors = [ArchiveFrameError]()

        func writeToStream(
            contact: BackupProto_Contact,
            contactAddress: BackupArchive.ContactAddress,
            contactDbRowId: SignalRecipient.RowId?,
            frameBencher: BackupArchive.Bencher.FrameBencher,
        ) {
            let maybeError: ArchiveFrameError? = Self.writeFrameToStream(
                stream,
                objectId: .contact(contactAddress),
                frameBencher: frameBencher,
                frameBuilder: {
                    let recipientAddress = contactAddress.asArchivingAddress()
                    let recipientId = context.assignRecipientId(to: recipientAddress)
                    if let contactDbRowId {
                        context.associateRecipientId(recipientId, withRecipientDbRowId: contactDbRowId)
                    }

                    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, BackupArchive.Bencher.FrameBencher) -> Void = { recipient, frameBencher in
            guard
                let contactAddress = BackupArchive.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.
                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 {
                identity = try OWSRecipientIdentity
                    .filter(Column(OWSRecipientIdentity.CodingKeys.uniqueId) == recipient.uniqueId)
                    .fetchOne(context.tx.database)
            } catch let error {
                errors.append(.archiveFrameError(
                    .unableToFetchRecipientIdentity(error),
                    .contact(contactAddress),
                ))
                return
            }

            let username: String? = recipient.aci
                .flatMap { aci in
                    self.usernameLookupManager.fetchUsername(
                        forAci: aci,
                        transaction: context.tx,
                    )
                }
                .flatMap { username in
                    // Pass through LibSignal to validate the username
                    try? Username(username).value
                }

            let contact = self.buildContactRecipient(
                aci: contactAddress.aci,
                pni: contactAddress.pni,
                e164: contactAddress.e164,
                username: username,
                nicknameRecord: self.nicknameManager.fetchNickname(
                    for: recipient,
                    tx: context.tx,
                ),
                isBlocked: blockedRecipientIds.contains(recipient.id),
                isWhitelisted: whitelistedAddresses.contains(recipient.address),
                isStoryHidden: isStoryHidden,
                visibility: { () -> BackupProto_Contact.Visibility in
                    guard
                        let hiddenRecipient = self.recipientHidingManager.fetchHiddenRecipient(
                            recipientId: recipient.id,
                            tx: context.tx,
                        )
                    else {
                        return .visible
                    }

                    if
                        self.recipientHidingManager.isHiddenRecipientThreadInMessageRequest(
                            hiddenRecipient: hiddenRecipient,
                            contactThread: self.threadStore.fetchContactThread(
                                recipient: recipient,
                                tx: context.tx,
                            ),
                            tx: context.tx,
                        )
                    {
                        return .hiddenMessageRequest
                    } else {
                        return .hidden
                    }
                }(),
                registration: { () -> BackupProto_Contact.OneOf_Registration in
                    if !recipient.isRegistered {
                        var notRegistered = BackupProto_Contact.NotRegistered()
                        if
                            let unregisteredTimestamp = recipient.unregisteredAtTimestamp,
                            BackupArchive.Timestamps.isValid(unregisteredTimestamp)
                        {
                            notRegistered.unregisteredTimestamp = unregisteredTimestamp
                        } else {
                            notRegistered.unregisteredTimestamp = SignalRecipient.Constants.distantPastUnregisteredTimestamp
                        }

                        return .notRegistered(notRegistered)
                    }

                    return .registered(BackupProto_Contact.Registered())
                }(),
                userProfile: self.profileManager.getUserProfile(
                    for: recipient.address,
                    tx: context.tx,
                ),
                identity: identity,
                signalAccount: self.contactManager.fetchSignalAccount(
                    recipient.address,
                    tx: context.tx,
                ),
                defaultAvatarColor: self.avatarDefaultColorManager.defaultColor(
                    useCase: .contact(recipient: recipient),
                    tx: context.tx,
                ),
                keyTransparencyBlob: recipient.aci.flatMap { aci in
                    self.keyTransparencyStore.getKeyTransparencyBlob(aci: aci, tx: context.tx)
                },
            )

            writeToStream(contact: contact, contactAddress: contactAddress, contactDbRowId: recipient.id, frameBencher: frameBencher)
        }

        do {
            try context.bencher.wrapEnumeration(
                recipientStore.enumerateAllSignalRecipients(tx:block:),
                tx: context.tx,
            ) { recipient, frameBencher in
                autoreleasepool {
                    recipientBlock(recipient, frameBencher)
                }
            }
        } 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.
        context.bencher.wrapEnumeration(
            profileManager.enumerateUserProfiles(tx:block:),
            tx: context.tx,
        ) { userProfile, frameBencher 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 = BackupArchive.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.
                    return
                }

                let signalServiceAddress: BackupArchive.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 = self.buildContactRecipient(
                    aci: contactAddress.aci,
                    pni: contactAddress.pni,
                    e164: contactAddress.e164,
                    username: nil, // If we have a user profile, we have no username.
                    nicknameRecord: nil, // Only contacts with SignalRecipients can have nicknames.
                    isBlocked: false, // Only contacts with SignalRecipients can be blocked.
                    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,
                    signalAccount: nil,
                    defaultAvatarColor: self.avatarDefaultColorManager.defaultColor(
                        useCase: .contactWithoutRecipient(address: contactAddress.asInteropAddress()),
                        tx: context.tx,
                    ),
                    keyTransparencyBlob: nil, // Can't do Key Transparency without a recipient
                )

                writeToStream(
                    contact: contact,
                    contactAddress: contactAddress,
                    contactDbRowId: nil,
                    frameBencher: frameBencher,
                )
            }
        }

        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: BackupArchive.ContactAddress,
        stream: BackupArchiveProtoOutputStream,
        frameBencher: BackupArchive.Bencher.FrameBencher,
        context: BackupArchive.ChatArchivingContext,
    ) -> BackupArchive.ArchiveSingleFrameResult<RecipientId, BackupArchive.ThreadUniqueId> {
        let existingRecipient = recipientStore.fetchRecipient(
            for: address,
            tx: context.tx,
        )
        // 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,
            nicknameRecord: nil, // Only contacts with SignalRecipients can have nicknames.
            isBlocked: false, // only contacts with SignalRecipients can be blocked.
            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,
            signalAccount: nil,
            defaultAvatarColor: avatarDefaultColorManager.defaultColor(
                useCase: .contactWithoutRecipient(address: address.asInteropAddress()),
                tx: context.tx,
            ),
            keyTransparencyBlob: nil,
        )

        let recipientAddress = address.asArchivingAddress()
        let recipientId = context.recipientContext.assignRecipientId(to: recipientAddress)

        let maybeError: BackupArchive.ArchiveFrameError<BackupArchive.ThreadUniqueId>?
        maybeError = Self.writeFrameToStream(
            stream,
            objectId: .init(thread: contactThread),
            frameBencher: frameBencher,
            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?,
        nicknameRecord: NicknameRecord?,
        isBlocked: Bool,
        isWhitelisted: Bool,
        isStoryHidden: Bool,
        visibility: BackupProto_Contact.Visibility,
        registration: BackupProto_Contact.OneOf_Registration,
        userProfile: OWSUserProfile?,
        identity: OWSRecipientIdentity?,
        signalAccount: SignalAccount?,
        defaultAvatarColor: AvatarTheme,
        keyTransparencyBlob: Data?,
    ) -> 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 = 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
        }
        if
            let nicknameRecord,
            // The rule is we don't set the nickname proto if both are nil
            !(nicknameRecord.givenName.isEmptyOrNil && nicknameRecord.familyName.isEmptyOrNil)
        {
            var nicknameProto = BackupProto_Contact.Name()
            if let givenName = nicknameRecord.givenName?.nilIfEmpty {
                nicknameProto.given = givenName
            }
            if let familyName = nicknameRecord.familyName?.nilIfEmpty {
                nicknameProto.family = familyName
            }
            contact.nickname = nicknameProto
        }
        if let note = nicknameRecord?.note?.nilIfEmpty {
            contact.note = note
        }
        if let signalAccount {
            contact.systemGivenName = signalAccount.givenName
            contact.systemFamilyName = signalAccount.familyName
            contact.systemNickname = signalAccount.nickname
        }
        contact.avatarColor = defaultAvatarColor.asBackupProtoAvatarColor

        if let keyTransparencyBlob {
            contact.keyTransparencyData = keyTransparencyBlob
        }

        return contact
    }

    // MARK: -

    func restoreContactRecipientProto(
        _ contactProto: BackupProto_Contact,
        recipient: BackupProto_Recipient,
        context: BackupArchive.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:
            // We treat unknown values as registered.
            isRegistered = true
            unregisteredTimestamp = nil
        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(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(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 = BackupArchive.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 deviceIds: [DeviceId]
        if isRegistered {
            // If we think they are registered, just add the primary device id.
            // When we try and send a message, the server will tell us about
            // any other device ids.
            // ...The server would tell us too if we sent an empty deviceIds array,
            // so there's not really a material difference.
            deviceIds = [.primary]
        } else {
            // Otherwise (including if we don't know if they're registered),
            // use an empty device IDs array. This doesn't make any difference,
            // the server will give us the deviceIds anyway and unregisteredAtTimestamp
            // is the thing that actually drives unregistered state, but
            // this is at least a better representation of what we know.
            deviceIds = []
        }

        let recipientProto = recipient
        var recipient: SignalRecipient
        do throws(GRDB.DatabaseError) {
            recipient = try SignalRecipient.insertRecord(
                aci: backupContactAddress.aci,
                phoneNumber: backupContactAddress.e164,
                pni: backupContactAddress.pni,
                deviceIds: deviceIds,
                unregisteredAtTimestamp: unregisteredTimestamp,
                tx: context.tx,
            )
            recipientStore.didInsertRecipient(recipient, tx: context.tx)
        } catch {
            return .failure([.restoreFrameError(.databaseInsertionFailed(error), recipientProto.recipientId)])
        }
        context.setRecipientDbRowId(recipient.id, forBackupRecipientId: 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
            } 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:
                verificationState = .default
            }

            // 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(
                uniqueId: recipient.uniqueId,
                identityKey: identityKey,
                isFirstKnownKey: true,
                createdAt: context.startDate,
                verificationState: verificationState,
            )
            do {
                try identity.insert(context.tx.database)
            } catch {
                return .failure([.restoreFrameError(
                    .databaseInsertionFailed(error),
                    recipientProto.recipientId,
                )])
            }
        }

        let nicknameGivenName = contactProto.nickname.given.nilIfEmpty
        let nicknameFamilyName = contactProto.nickname.family.nilIfEmpty
        let nicknameNote = contactProto.note.nilIfEmpty
        if nicknameGivenName != nil || nicknameFamilyName != nil || nicknameNote != nil {
            let nicknameRecord = NicknameRecord(
                recipient: recipient,
                givenName: nicknameGivenName,
                familyName: nicknameFamilyName,
                note: nicknameNote,
            )
            self.nicknameManager.createOrUpdate(
                nicknameRecord: nicknameRecord,
                updateStorageServiceFor: nil,
                tx: context.tx,
            )
        }

        if contactProto.profileSharing {
            // Add to the whitelist.
            profileManager.addRecipientToProfileWhitelist(&recipient, 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 = [BackupArchive.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,
        )

        let systemGivenName = contactProto.systemGivenName
        let systemFamilyName = contactProto.systemFamilyName
        let systemNickname = contactProto.systemNickname
        let systemFullName = Contact.fullName(
            fromGivenName: systemGivenName,
            familyName: systemFamilyName,
            nickname: systemNickname,
        )
        if let systemFullName {
            let systemContact = SignalAccount(
                recipientPhoneNumber: backupContactAddress.e164?.stringValue,
                recipientServiceId: backupContactAddress.aci ?? backupContactAddress.pni,
                multipleAccountLabelText: nil,
                cnContactId: nil,
                givenName: systemGivenName,
                familyName: systemFamilyName,
                nickname: systemNickname,
                fullName: systemFullName,
                contactAvatarHash: nil,
            )

            contactManager.insertSignalAccount(systemContact, tx: context.tx)
        }

        if
            contactProto.hasAvatarColor,
            let defaultAvatarColor: AvatarTheme = .from(backupProtoAvatarColor: contactProto.avatarColor)
        {
            do {
                try avatarDefaultColorManager.persistDefaultColor(
                    defaultAvatarColor,
                    recipientRowId: recipient.id,
                    tx: context.tx,
                )
            } catch let error {
                partialErrors.append(.restoreFrameError(.databaseInsertionFailed(error), recipientProto.recipientId))
            }
        }

        if
            contactProto.hasKeyTransparencyData,
            let aci = backupContactAddress.aci
        {
            keyTransparencyStore.setKeyTransparencyBlob(
                contactProto.keyTransparencyData,
                aci: aci,
                tx: context.tx,
            )
        }

        if partialErrors.isEmpty {
            return .success
        } else {
            return .partialRestore(partialErrors)
        }
    }
}