Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Backups/Archiving/Archivers/AccountData/BackupArchiveAccountDataArchiver.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import LibSignalClient

extension BackupArchive {
    /// An identifier for the ``BackupProto_AccountData`` backup frame.
    ///
    /// Uses a singleton pattern, as there is only ever one account data frame
    /// in a backup.
    public struct AccountDataId: BackupArchive.LoggableId {
        static let localUser = AccountDataId()

        static func forCustomChatColorError(chatColorId: CustomChatColorId) -> Self {
            return .init(chatColorId)
        }

        /// Since custom chat colors are included in account data, errors can be nested
        /// with an account data -> chat color id
        private let chatColorId: CustomChatColorId?

        private init(_ chatColorId: CustomChatColorId? = nil) {
            self.chatColorId = chatColorId
        }

        // MARK: BackupArchive.LoggableId

        public var typeLogString: String {
            if chatColorId != nil {
                return "BackupProto_AccountData_CustomChatColor"
            } else {
                return "BackupProto_AccountData"
            }
        }

        public var idLogString: String {
            if let chatColorId {
                return "localUser_\(chatColorId.value)"
            } else {
                return "localUser"
            }
        }
    }

    public typealias ArchiveAccountDataResult = ArchiveSingleFrameResult<Void, AccountDataId>
    public typealias RestoreAccountDataResult = RestoreFrameResult<BackupArchive.AccountDataId>
}

/// Archives the ``BackupProto_AccountData`` frame.
public class BackupArchiveAccountDataArchiver: BackupArchiveProtoStreamWriter {
    private let backupAttachmentUploadEraStore: BackupAttachmentUploadEraStore
    private let backupSettingsStore: BackupSettingsStore
    private let backupSubscriptionManager: BackupSubscriptionManager
    private let callServiceSettingsStore: CallServiceSettingsStore
    private let chatStyleArchiver: BackupArchiveChatStyleArchiver
    private let disappearingMessageConfigurationStore: DisappearingMessagesConfigurationStore
    private let donationSubscriptionManager: BackupArchive.Shims.DonationSubscriptionManager
    private let imageQuality: BackupArchive.Shims.ImageQuality
    private let keyTransparencyManager: KeyTransparencyManager
    private let keyTransparencyStore: KeyTransparencyStore
    private let linkPreviewSettingStore: LinkPreviewSettingStore
    private let localUsernameManager: LocalUsernameManager
    private let logger: PrefixedLogger
    private let mediaBandwidthPreferenceStore: MediaBandwidthPreferenceStore
    private let ows2FAManager: BackupArchive.Shims.OWS2FAManager
    private let phoneNumberDiscoverabilityManager: PhoneNumberDiscoverabilityManager
    private let preferences: BackupArchive.Shims.Preferences
    private let profileManager: BackupArchive.Shims.ProfileManager
    private let receiptManager: BackupArchive.Shims.ReceiptManager
    private let reactionManager: BackupArchive.Shims.ReactionManager
    private let screenLock: BackupArchive.Shims.ScreenLock
    private let sskPreferences: BackupArchive.Shims.SSKPreferences
    private let storyManager: BackupArchive.Shims.StoryManager
    private let systemStoryManager: BackupArchive.Shims.SystemStoryManager
    private let theme: ThemeDataStore
    private let typingIndicators: BackupArchive.Shims.TypingIndicators
    private let udManager: BackupArchive.Shims.UDManager
    private let usernameEducationManager: UsernameEducationManager

    public init(
        backupAttachmentUploadEraStore: BackupAttachmentUploadEraStore,
        backupSettingsStore: BackupSettingsStore,
        backupSubscriptionManager: BackupSubscriptionManager,
        callServiceSettingsStore: CallServiceSettingsStore,
        chatStyleArchiver: BackupArchiveChatStyleArchiver,
        disappearingMessageConfigurationStore: DisappearingMessagesConfigurationStore,
        donationSubscriptionManager: BackupArchive.Shims.DonationSubscriptionManager,
        imageQuality: BackupArchive.Shims.ImageQuality,
        keyTransparencyManager: KeyTransparencyManager,
        keyTransparencyStore: KeyTransparencyStore,
        linkPreviewSettingStore: LinkPreviewSettingStore,
        localUsernameManager: LocalUsernameManager,
        mediaBandwidthPreferenceStore: MediaBandwidthPreferenceStore,
        ows2FAManager: BackupArchive.Shims.OWS2FAManager,
        phoneNumberDiscoverabilityManager: PhoneNumberDiscoverabilityManager,
        preferences: BackupArchive.Shims.Preferences,
        profileManager: BackupArchive.Shims.ProfileManager,
        receiptManager: BackupArchive.Shims.ReceiptManager,
        reactionManager: BackupArchive.Shims.ReactionManager,
        screenLock: BackupArchive.Shims.ScreenLock,
        sskPreferences: BackupArchive.Shims.SSKPreferences,
        storyManager: BackupArchive.Shims.StoryManager,
        systemStoryManager: BackupArchive.Shims.SystemStoryManager,
        theme: ThemeDataStore,
        typingIndicators: BackupArchive.Shims.TypingIndicators,
        udManager: BackupArchive.Shims.UDManager,
        usernameEducationManager: UsernameEducationManager,
    ) {
        self.backupAttachmentUploadEraStore = backupAttachmentUploadEraStore
        self.backupSettingsStore = backupSettingsStore
        self.backupSubscriptionManager = backupSubscriptionManager
        self.callServiceSettingsStore = callServiceSettingsStore
        self.chatStyleArchiver = chatStyleArchiver
        self.disappearingMessageConfigurationStore = disappearingMessageConfigurationStore
        self.donationSubscriptionManager = donationSubscriptionManager
        self.imageQuality = imageQuality
        self.keyTransparencyManager = keyTransparencyManager
        self.keyTransparencyStore = keyTransparencyStore
        self.linkPreviewSettingStore = linkPreviewSettingStore
        self.localUsernameManager = localUsernameManager
        self.logger = PrefixedLogger(prefix: "[Backups]")
        self.mediaBandwidthPreferenceStore = mediaBandwidthPreferenceStore
        self.ows2FAManager = ows2FAManager
        self.phoneNumberDiscoverabilityManager = phoneNumberDiscoverabilityManager
        self.preferences = preferences
        self.profileManager = profileManager
        self.receiptManager = receiptManager
        self.reactionManager = reactionManager
        self.screenLock = screenLock
        self.sskPreferences = sskPreferences
        self.storyManager = storyManager
        self.systemStoryManager = systemStoryManager
        self.theme = theme
        self.typingIndicators = typingIndicators
        self.udManager = udManager
        self.usernameEducationManager = usernameEducationManager
    }

    // MARK: -

    func archiveAccountData(
        stream: BackupArchiveProtoOutputStream,
        context: BackupArchive.CustomChatColorArchivingContext,
    ) -> BackupArchive.ArchiveAccountDataResult {
        return context.bencher.processFrame { frameBencher in
            guard let localProfile = profileManager.getUserProfileForLocalUser(tx: context.tx) else {
                return .failure(.archiveFrameError(.missingLocalProfile, .localUser))
            }
            guard let profileKeyData = localProfile.profileKey?.keyData else {
                return .failure(.archiveFrameError(.missingLocalProfileKey, .localUser))
            }

            var accountData = BackupProto_AccountData()
            accountData.profileKey = profileKeyData
            accountData.givenName = localProfile.givenName ?? ""
            accountData.familyName = localProfile.familyName ?? ""
            accountData.avatarURLPath = localProfile.avatarUrlPath ?? ""
            accountData.bioText = localProfile.bio ?? ""
            accountData.bioEmoji = localProfile.bioEmoji ?? ""

            if let donationSubscriberId = donationSubscriptionManager.getSubscriberID(tx: context.tx) {
                var donationSubscriberData = BackupProto_AccountData.SubscriberData()
                donationSubscriberData.subscriberID = donationSubscriberId
                donationSubscriberData.currencyCode = donationSubscriptionManager.getSubscriberCurrencyCode(tx: context.tx) ?? ""
                donationSubscriberData.manuallyCancelled = donationSubscriptionManager.userManuallyCancelledSubscription(tx: context.tx)

                accountData.donationSubscriberData = donationSubscriberData
            }

            if let result = buildUsernameLinkProto(context: context) {
                accountData.username = result.username
                accountData.usernameLink = result.usernameLink
            }

            let accountSettingsResult = buildAccountSettingsProto(context: context)
            switch accountSettingsResult {
            case .success(let accountSettings):
                accountData.accountSettings = accountSettings
            case .failure(let error):
                return .failure(error)
            }

            if let iapSubscriberData = backupSubscriptionManager.getIAPSubscriberData(tx: context.tx) {
                var iapSubscriberDataProto = BackupProto_AccountData.IAPSubscriberData()
                iapSubscriberDataProto.subscriberID = iapSubscriberData.subscriberId
                iapSubscriberDataProto.iapSubscriptionID = switch iapSubscriberData.iapSubscriptionId {
                case .originalTransactionId(let value): .originalTransactionID(value)
                case .purchaseToken(let value): .purchaseToken(value)
                }

                accountData.backupsSubscriberData = iapSubscriberDataProto
            }

            if
                context.includedContentFilter.shouldIncludePin,
                let pin = ows2FAManager.getPin(tx: context.tx)
            {
                accountData.svrPin = pin
            }

            if
                let keyTransparencyBlob = keyTransparencyStore.getKeyTransparencyBlob(
                    aci: context.localIdentifiers.aci,
                    tx: context.tx,
                )
            {
                accountData.keyTransparencyData = keyTransparencyBlob
            }

            if let isSystemCallLogEnabled = preferences.isSystemCallLogEnabled(tx: context.tx) {
                var iosSpecificSettings = BackupProto_AccountData.IOSSpecificSettings()
                iosSpecificSettings.isSystemCallLogEnabled = isSystemCallLogEnabled
                accountData.iosSpecificSettings = iosSpecificSettings
            }

            let error = Self.writeFrameToStream(
                stream,
                objectId: BackupArchive.AccountDataId.localUser,
                frameBencher: frameBencher,
            ) {
                var frame = BackupProto_Frame()
                frame.item = .account(accountData)
                return frame
            }

            if let error {
                return .failure(error)
            } else {
                return .success(())
            }
        }
    }

    private func buildUsernameLinkProto(
        context: BackupArchive.ArchivingContext,
    ) -> (username: String, usernameLink: BackupProto_AccountData.UsernameLink)? {
        switch self.localUsernameManager.usernameState(tx: context.tx) {
        case .unset, .linkCorrupted, .usernameAndLinkCorrupted:
            return nil
        case .available(let username, let usernameLink):
            var usernameLinkProto = BackupProto_AccountData.UsernameLink()
            usernameLinkProto.entropy = usernameLink.entropy
            usernameLinkProto.serverID = usernameLink.handle.data
            usernameLinkProto.color = localUsernameManager.usernameLinkQRCodeColor(tx: context.tx).backupProtoColor

            return (username, usernameLinkProto)
        }
    }

    private func buildAccountSettingsProto(
        context: BackupArchive.CustomChatColorArchivingContext,
    ) -> BackupArchive.ArchiveSingleFrameResult<BackupProto_AccountData.AccountSettings, BackupArchive.AccountDataId> {

        // Fetch all the account settings
        let readReceipts = receiptManager.areReadReceiptsEnabled(tx: context.tx)
        let sealedSenderIndicators = preferences.shouldShowUnidentifiedDeliveryIndicators(tx: context.tx)
        let typingIndicatorsEnabled = typingIndicators.areTypingIndicatorsEnabled()
        let linkPreviews = linkPreviewSettingStore.areLinkPreviewsEnabled(tx: context.tx)
        let notDiscoverableByPhoneNumber = switch phoneNumberDiscoverabilityManager.phoneNumberDiscoverability(tx: context.tx) {
        case .everybody: false
        case .nobody, .none: true
        }
        let preferContactAvatars = sskPreferences.preferContactAvatars(tx: context.tx)
        let universalExpireTimerSeconds = disappearingMessageConfigurationStore.fetchOrBuildDefault(
            for: .universal,
            tx: context.tx,
        ).durationSeconds
        let displayBadgesOnProfile = donationSubscriptionManager.displayBadgesOnProfile(tx: context.tx)
        let keepMutedChatsArchived = sskPreferences.shouldKeepMutedChatsArchived(tx: context.tx)
        let hasSetMyStoriesPrivacy = storyManager.hasSetMyStoriesPrivacy(tx: context.tx)
        let hasViewedOnboardingStory = systemStoryManager.isOnboardingStoryViewed(tx: context.tx)
        let storiesDisabled = storyManager.areStoriesEnabled(tx: context.tx).negated
        let hasSeenGroupStoryEducationSheet = systemStoryManager.hasSeenGroupStoryEducationSheet(tx: context.tx)
        let hasCompletedUsernameOnboarding = usernameEducationManager.shouldShowUsernameEducation(tx: context.tx).negated
        let phoneNumberSharingMode: BackupProto_AccountData.PhoneNumberSharingMode = switch udManager.phoneNumberSharingMode(tx: context.tx).orDefault {
        case .everybody: .everybody
        case .nobody: .nobody
        }
        let hasPinReminders = ows2FAManager.areRemindersEnabled(tx: context.tx)

        // Populate the proto with the settings
        var accountSettings = BackupProto_AccountData.AccountSettings()
        accountSettings.readReceipts = readReceipts
        accountSettings.sealedSenderIndicators = sealedSenderIndicators
        accountSettings.typingIndicators = typingIndicatorsEnabled
        accountSettings.linkPreviews = linkPreviews
        accountSettings.notDiscoverableByPhoneNumber = notDiscoverableByPhoneNumber
        accountSettings.preferContactAvatars = preferContactAvatars
        accountSettings.universalExpireTimerSeconds = universalExpireTimerSeconds
        accountSettings.displayBadgesOnProfile = displayBadgesOnProfile
        accountSettings.keepMutedChatsArchived = keepMutedChatsArchived
        accountSettings.hasSetMyStoriesPrivacy_p = hasSetMyStoriesPrivacy
        accountSettings.hasViewedOnboardingStory_p = hasViewedOnboardingStory
        accountSettings.storiesDisabled = storiesDisabled
        accountSettings.hasSeenGroupStoryEducationSheet_p = hasSeenGroupStoryEducationSheet
        accountSettings.hasCompletedUsernameOnboarding_p = hasCompletedUsernameOnboarding
        accountSettings.phoneNumberSharingMode = phoneNumberSharingMode
        accountSettings.preferredReactionEmoji = reactionManager.customEmojiSet(tx: context.tx) ?? []
        accountSettings.storyViewReceiptsEnabled = storyManager.areViewReceiptsEnabled(tx: context.tx)
        accountSettings.pinReminders = hasPinReminders
        switch backupSettingsStore.backupPlan(tx: context.tx) {
        case .disabling, .disabled:
            accountSettings.clearBackupTier()
            accountSettings.optimizeOnDeviceStorage = false
        case .free:
            accountSettings.backupTier = UInt64(BackupLevel.free.rawValue)
            accountSettings.optimizeOnDeviceStorage = false
        case .paid(let optimizeLocalStorage), .paidExpiringSoon(let optimizeLocalStorage), .paidAsTester(let optimizeLocalStorage):
            accountSettings.backupTier = UInt64(BackupLevel.paid.rawValue)
            accountSettings.optimizeOnDeviceStorage = optimizeLocalStorage
        }

        let customChatColorsResult = chatStyleArchiver.archiveCustomChatColors(
            context: context,
        )
        switch customChatColorsResult {
        case .success(let customChatColors):
            accountSettings.customChatColors = customChatColors
        case .failure(let error):
            return .failure(error)
        }

        // This has to happen _after_ we archive custom chat colors, because
        // the default chat style might use a custom chat color.
        let defaultChatStyleResult = chatStyleArchiver.archiveDefaultChatStyle(
            context: context,
        )
        switch defaultChatStyleResult {
        case .success(let chatStyleProto):
            if let chatStyleProto {
                accountSettings.defaultChatStyle = chatStyleProto
            }
        case .failure(let archiveFrameError):
            return .failure(archiveFrameError)
        }

        accountSettings.allowSealedSenderFromAnyone = udManager.shouldAllowUnrestrictedAccessLocal(tx: context.tx)
        accountSettings.allowAutomaticKeyVerification = keyTransparencyManager.isEnabled(tx: context.tx)
        accountSettings.defaultSentMediaQuality = imageQuality.fetchValue(tx: context.tx) == .high ? .high : .standard

        var downloadSettings = BackupProto_AccountData.AutoDownloadSettings()
        for type in MediaBandwidthPreferences.MediaType.allCases {
            let setting = mediaBandwidthPreferenceStore.preference(for: type, tx: context.tx)
            switch type {
            case .audio:
                downloadSettings.audio = setting.backupProtoPreference
            case .video:
                downloadSettings.video = setting.backupProtoPreference
            case .document:
                downloadSettings.documents = setting.backupProtoPreference
            case .photo:
                downloadSettings.images = setting.backupProtoPreference
            }
        }
        accountSettings.autoDownloadSettings = downloadSettings

        if screenLock.isScreenLockEnabled(tx: context.tx) {
            let screenLockSeconds = screenLock.screenLockTimeout(tx: context.tx)
            accountSettings.screenLockTimeoutMinutes = UInt32(screenLockSeconds / Double(60))
        }

        accountSettings.appTheme = switch theme.getCurrentMode(tx: context.tx) {
        case .dark: .dark
        case .light: .light
        case .system: .system
        }

        let callServiceDataSetting = callServiceSettingsStore.highDataNetworkInterfaces(tx: context.tx)
        accountSettings.callsUseLessDataSetting = if callServiceDataSetting == .wifiAndCellular {
            .wifiAndMobileData
        } else if callServiceDataSetting == .cellular {
            .mobileDataOnly
        } else {
            .never
        }

        return .success(accountSettings)
    }

    // MARK: -

    func restore(
        _ accountData: BackupProto_AccountData,
        context: BackupArchive.AccountDataRestoringContext,
        chatColorsContext: BackupArchive.CustomChatColorRestoringContext,
        chatItemContext: BackupArchive.ChatItemRestoringContext,
    ) -> BackupArchive.RestoreAccountDataResult {
        guard let profileKey = Aes256Key(data: accountData.profileKey) else {
            return .failure([.restoreFrameError(
                .invalidProtoData(.invalidLocalProfileKey),
                .localUser,
            )])
        }

        var partialErrors = [BackupArchive.RestoreFrameError<BackupArchive.AccountDataId>]()

        // Given name and profile key are required for the local profile. The
        // rest are optional.
        profileManager.insertLocalUserProfile(
            givenName: accountData.givenName,
            familyName: accountData.familyName.nilIfEmpty,
            avatarUrlPath: accountData.avatarURLPath.nilIfEmpty,
            bio: accountData.bioText.nilIfEmpty,
            bioEmoji: accountData.bioEmoji.nilIfEmpty,
            profileKey: profileKey,
            tx: context.tx,
        )

        // Restore donation subscription data, if present.
        if accountData.hasDonationSubscriberData {
            let donationSubscriberData = accountData.donationSubscriberData
            donationSubscriptionManager.setSubscriberID(subscriberID: donationSubscriberData.subscriberID, tx: context.tx)
            donationSubscriptionManager.setSubscriberCurrencyCode(currencyCode: donationSubscriberData.currencyCode, tx: context.tx)
            donationSubscriptionManager.setUserManuallyCancelledSubscription(value: donationSubscriberData.manuallyCancelled, tx: context.tx)
        }

        if
            accountData.hasBackupsSubscriberData,
            let subscriberID = accountData.backupsSubscriberData.subscriberID.nilIfEmpty,
            let protoIapSubscriberID = accountData.backupsSubscriberData.iapSubscriptionID
        {
            typealias IAPSubscriptionID = BackupSubscriptionManager.IAPSubscriberData.IAPSubscriptionId
            let iapSubscriptionID: IAPSubscriptionID = switch protoIapSubscriberID {
            case .purchaseToken(let value):
                .purchaseToken(value)
            case .originalTransactionID(let value):
                .originalTransactionId(value)
            }

            backupSubscriptionManager.restoreIAPSubscriberData(
                BackupSubscriptionManager.IAPSubscriberData(
                    subscriberId: subscriberID,
                    iapSubscriptionId: iapSubscriptionID,
                ),
                tx: context.tx,
            )
        }

        let backupLevel: BackupLevel?
        if accountData.accountSettings.hasBackupTier {
            guard
                let parsedLevel =
                UInt8(exactly: accountData.accountSettings.backupTier)
                    .map(BackupLevel.init(rawValue:))
            else {
                return .failure([.restoreFrameError(
                    .invalidProtoData(.invalidBackupTier),
                    .localUser,
                )])
            }
            backupLevel = parsedLevel
        } else {
            // Backups disabled
            backupLevel = nil
        }

        let backupPlan: BackupPlan
        let uploadEra: String
        switch backupLevel {
        case .paid:
            uploadEra = backupAttachmentUploadEraStore.currentUploadEra(tx: context.tx)

            let optimizeLocalStorage = accountData.accountSettings.optimizeOnDeviceStorage
            if BuildFlags.Backups.avoidStoreKitForTesters {
                // If we're importing into a build that can't make purchases,
                // opt ourselves into "paid as tester" mode. We'll manage IAP
                // data, if there is any, separately.
                backupPlan = .paidAsTester(optimizeLocalStorage: optimizeLocalStorage)
            } else {
                backupPlan = .paid(optimizeLocalStorage: optimizeLocalStorage)
            }
        case .free:
            // The exporting client was not subscribed at export time.
            // It may have subscribed after, or not. We don't know, and we don't
            // know if it was previously subscribed and if so what its subscriberId was.
            // So we use the local era (which is probably the initial era), and assume
            // we're in the same one now as we were at export time.
            // If we subscribe (or learn about a subscription) the era will rotate,
            // invalidating our belief in any attachment uploads.
            // Querying the list endpoint will bring us up to date on the upload status
            // within this "era" otherwise.
            uploadEra = backupAttachmentUploadEraStore.currentUploadEra(tx: context.tx)
            backupPlan = .free
        case .none:
            // See above comment; the same applies
            uploadEra = backupAttachmentUploadEraStore.currentUploadEra(tx: context.tx)
            backupPlan = .disabled
        }
        logger.info("Setting BackupPlan during restore: \(backupPlan)")
        backupSettingsStore.setBackupPlan(backupPlan, tx: context.tx)

        // These MUST get set before we restore custom chat colors/wallpapers.
        context.uploadEra = uploadEra
        context.backupPlan = backupPlan

        // Restore local settings
        if accountData.hasAccountSettings {
            let settings = accountData.accountSettings
            receiptManager.setAreReadReceiptsEnabled(value: settings.readReceipts, tx: context.tx)
            preferences.setShouldShowUnidentifiedDeliveryIndicators(value: settings.sealedSenderIndicators, tx: context.tx)
            typingIndicators.setTypingIndicatorsEnabled(value: settings.typingIndicators, tx: context.tx)
            linkPreviewSettingStore.setAreLinkPreviewsEnabled(settings.linkPreviews, tx: context.tx)
            phoneNumberDiscoverabilityManager.setPhoneNumberDiscoverability(
                settings.notDiscoverableByPhoneNumber ? .nobody : .everybody,
                updateAccountAttributes: false, // This should be updated later, similar to storage service
                updateStorageService: false,
                authedAccount: .implicit(),
                tx: context.tx,
            )
            sskPreferences.setPreferContactAvatars(value: settings.preferContactAvatars, tx: context.tx)
            disappearingMessageConfigurationStore.setUniversalTimer(
                token: DisappearingMessageToken(
                    isEnabled: settings.universalExpireTimerSeconds > 0,
                    durationSeconds: settings.universalExpireTimerSeconds,
                ),
                tx: context.tx,
            )
            if settings.preferredReactionEmoji.count > 0 {
                reactionManager.setCustomEmojiSet(emojis: settings.preferredReactionEmoji, tx: context.tx)
            }
            donationSubscriptionManager.setDisplayBadgesOnProfile(value: settings.displayBadgesOnProfile, tx: context.tx)
            sskPreferences.setShouldKeepMutedChatsArchived(value: settings.keepMutedChatsArchived, tx: context.tx)
            storyManager.setHasSetMyStoriesPrivacy(value: settings.hasSetMyStoriesPrivacy_p, tx: context.tx)
            systemStoryManager.setHasViewedOnboardingStory(value: settings.hasViewedOnboardingStory_p, tx: context.tx)
            storyManager.setAreStoriesEnabled(value: settings.storiesDisabled.negated, tx: context.tx)
            if settings.hasStoryViewReceiptsEnabled {
                storyManager.setAreViewReceiptsEnabled(value: settings.storyViewReceiptsEnabled, tx: context.tx)
            }
            systemStoryManager.setHasSeenGroupStoryEducationSheet(value: settings.hasSeenGroupStoryEducationSheet_p, tx: context.tx)
            usernameEducationManager.setShouldShowUsernameEducation(settings.hasCompletedUsernameOnboarding_p.negated, tx: context.tx)
            udManager.setPhoneNumberSharingMode(
                mode: { () -> PhoneNumberSharingMode in
                    switch settings.phoneNumberSharingMode {
                    case .unknown, .UNRECOGNIZED:
                        return .defaultValue
                    case .everybody:
                        return .everybody
                    case .nobody:
                        return .nobody
                    }
                }(),
                tx: context.tx,
            )

            let customChatColorsResult = chatStyleArchiver.restoreCustomChatColors(
                settings.customChatColors,
                context: chatColorsContext,
            )
            switch customChatColorsResult {
            case .success:
                break
            case .unrecognizedEnum:
                return customChatColorsResult
            case .partialRestore(let errors):
                partialErrors.append(contentsOf: errors)
            case .failure(let errors):
                partialErrors.append(contentsOf: errors)
                return .failure(partialErrors)
            }

            // This has to happen _after_ we restore custom chat colors, because
            // the default chat style might use a custom chat color.
            let defaultChatStyleToRestore: BackupProto_ChatStyle?
            if settings.hasDefaultChatStyle {
                defaultChatStyleToRestore = settings.defaultChatStyle
            } else {
                defaultChatStyleToRestore = nil
            }
            let defaultChatStyleResult = chatStyleArchiver.restoreDefaultChatStyle(
                defaultChatStyleToRestore,
                context: chatColorsContext,
            )
            switch defaultChatStyleResult {
            case .success:
                break
            case .unrecognizedEnum:
                return defaultChatStyleResult
            case .partialRestore(let errors):
                partialErrors.append(contentsOf: errors)
            case .failure(let errors):
                return .failure(errors)
            }

            udManager.setShouldAllowUnrestrictedAccessLocal(settings.allowSealedSenderFromAnyone, tx: context.tx)

            keyTransparencyManager.setIsEnabled(
                settings.allowAutomaticKeyVerification,
                updateStorageService: false,
                tx: context.tx,
            )

            switch settings.defaultSentMediaQuality {
            case .high:
                imageQuality.setValue(.high, tx: context.tx)
            case .unknownQuality, .UNRECOGNIZED, .standard:
                imageQuality.setValue(.standard, tx: context.tx)
            }

            if settings.hasAutoDownloadSettings {
                let mediaSettings = settings.autoDownloadSettings
                for type in MediaBandwidthPreferences.MediaType.allCases {
                    switch type {
                    case .audio:
                        mediaBandwidthPreferenceStore.set(
                            mediaSettings.audio.mediaBandwidthPreference,
                            for: .audio,
                            tx: context.tx,
                        )
                    case .video:
                        mediaBandwidthPreferenceStore.set(
                            mediaSettings.video.mediaBandwidthPreference,
                            for: .video,
                            tx: context.tx,
                        )
                    case .document:
                        mediaBandwidthPreferenceStore.set(
                            mediaSettings.documents.mediaBandwidthPreference,
                            for: .document,
                            tx: context.tx,
                        )
                    case .photo:
                        mediaBandwidthPreferenceStore.set(
                            mediaSettings.images.mediaBandwidthPreference,
                            for: .photo,
                            tx: context.tx,
                        )
                    }
                }
            }

            if settings.hasScreenLockTimeoutMinutes {
                let timeout = Double(settings.screenLockTimeoutMinutes * 60)
                screenLock.setIsScreenLockEnabled(true, tx: context.tx)
                screenLock.setScreenLockTimeout(timeout, tx: context.tx)
            }

            if settings.hasPinReminders {
                ows2FAManager.setAreRemindersEnabled(settings.pinReminders, tx: context.tx)
                if settings.pinReminders {
                    ows2FAManager.resetDefaultRepetitionIntervalForBackupRestore(tx: context.tx)
                }
            }

            let appAppearanceMode: ThemeDataStore.Appearance = switch settings.appTheme {
            case .UNRECOGNIZED, .unknownAppTheme, .system: .system
            case .dark: .dark
            case .light: .light
            }
            theme.setCurrentMode(appAppearanceMode, tx: context.tx)

            let callServiceDataMode: NetworkInterfaceSet = switch settings.callsUseLessDataSetting {
            case .mobileDataOnly: .cellular
            case .wifiAndMobileData: .wifiAndCellular
            case .never, .UNRECOGNIZED, .unknownCallDataSetting: .none
            }
            callServiceSettingsStore.setHighDataInterfaces(
                callServiceDataMode,
                tx: context.tx,
            )
        }

        // Restore username details (username, link, QR color)
        if accountData.hasUsername, accountData.hasUsernameLink {
            let username = accountData.username
            let usernameLink = accountData.usernameLink

            if
                let handle = UUID(data: usernameLink.serverID),
                let linkData = Usernames.UsernameLink(handle: handle, entropy: usernameLink.entropy)
            {
                localUsernameManager.setLocalUsername(username: username, usernameLink: linkData, tx: context.tx)
            } else {
                return .failure([.restoreFrameError(.invalidProtoData(.invalidLocalUsernameLink), .localUser)])
            }

            localUsernameManager.setUsernameLinkQRCodeColor(color: usernameLink.color.qrCodeColor, tx: context.tx)
        }

        if !accountData.svrPin.isEmpty {
            ows2FAManager.restorePinFromBackup(accountData.svrPin, tx: context.tx)
        }

        if accountData.hasKeyTransparencyData {
            keyTransparencyStore.setKeyTransparencyBlob(
                accountData.keyTransparencyData,
                aci: context.localIdentifiers.aci,
                tx: context.tx,
            )
        }

        if accountData.hasIosSpecificSettings {
            preferences.setIsSystemCallLogEnabled(
                accountData.iosSpecificSettings.isSystemCallLogEnabled,
                tx: context.tx,
            )
        }

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

// MARK: -

private extension QRCodeColor {
    var backupProtoColor: BackupProto_AccountData.UsernameLink.Color {
        switch self {
        case .blue: return .blue
        case .white: return .white
        case .grey: return .grey
        case .olive: return .olive
        case .green: return .green
        case .orange: return .orange
        case .pink: return .pink
        case .purple: return .purple
        }
    }
}

private extension BackupProto_AccountData.UsernameLink.Color {
    var qrCodeColor: QRCodeColor {
        switch self {
        case .blue: return .blue
        case .white: return .white
        case .grey: return .grey
        case .olive: return .olive
        case .green: return .green
        case .orange: return .orange
        case .pink: return .pink
        case .purple: return .purple
        case .unknown, .UNRECOGNIZED: return .unknown
        }
    }
}

private extension MediaBandwidthPreferences.Preference {
    var backupProtoPreference: BackupProto_AccountData.AutoDownloadSettings.AutoDownloadOption {
        switch self {
        case .never: return .never
        case .wifiOnly: return .wifi
        case .wifiAndCellular: return .wifiAndCellular
        }
    }
}

private extension BackupProto_AccountData.AutoDownloadSettings.AutoDownloadOption {
    var mediaBandwidthPreference: MediaBandwidthPreferences.Preference {
        switch self {
        case .never: return .never
        case .wifi: return .wifiOnly
        case .wifiAndCellular: return .wifiAndCellular
        case .unknown, .UNRECOGNIZED: return .never
        }
    }
}