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

import LibSignalClient
import SignalServiceKit

// MARK: -

final class BackupRefreshManager {
    private let accountKeyStore: AccountKeyStore
    private let api: NetworkAPI
    private let backupRequestManager: BackupRequestManager
    private let backupSettingsStore: BackupSettingsStore
    private let db: DB

    // Backups may expire after 30 days. Do a refresh every few days
    // to account for if the app isn't opened often.
    static let backupRefreshTimeInterval: TimeInterval = 3 * .day

    init(
        accountKeyStore: AccountKeyStore,
        backupRequestManager: BackupRequestManager,
        backupSettingsStore: BackupSettingsStore,
        db: DB,
        networkManager: NetworkManagerProtocol,
    ) {
        self.accountKeyStore = accountKeyStore
        self.api = NetworkAPI(networkManager: networkManager)
        self.backupRequestManager = backupRequestManager
        self.backupSettingsStore = backupSettingsStore
        self.db = db
    }

    private func rootBackupKeys(localIdentifiers: LocalIdentifiers) async throws -> (MessageRootBackupKey, MediaRootBackupKey) {
        try await db.awaitableWrite { tx in
            guard let messageRootBackupKey = try? accountKeyStore.getMessageRootBackupKey(aci: localIdentifiers.aci, tx: tx) else {
                throw OWSAssertionError("Missing message root backup key! Do we not have an AEP?")
            }

            // If we don't yet have an MRBK, this is an appropriate point to
            // generate one.
            let mediaRootBackupKey = accountKeyStore.getOrGenerateMediaRootBackupKey(tx: tx)

            return (messageRootBackupKey, mediaRootBackupKey)
        }
    }

    func refreshBackup(localIdentifiers: LocalIdentifiers, logger: PrefixedLogger) async throws {
        let backupPlan = db.read(block: backupSettingsStore.backupPlan(tx:))
        switch backupPlan {
        case .disabled:
            return
        case .disabling, .free, .paid, .paidExpiringSoon, .paidAsTester:
            break
        }

        let (messageBackupKey, mediaBackupKey) = try await rootBackupKeys(localIdentifiers: localIdentifiers)
        let messageBackupAuth = try await backupRequestManager.fetchBackupServiceAuth(
            for: messageBackupKey,
            localAci: localIdentifiers.aci,
            auth: .implicit(),
            logger: logger,
        )
        try await api.refreshBackup(auth: messageBackupAuth, logger: logger)

        let mediaBackupAuth = try await backupRequestManager.fetchBackupServiceAuth(
            for: mediaBackupKey,
            localAci: localIdentifiers.aci,
            auth: .implicit(),
            logger: logger,
        )
        try await api.refreshBackup(auth: mediaBackupAuth, logger: logger)
    }

    // MARK: -

    private struct NetworkAPI {
        private let networkManager: NetworkManagerProtocol

        init(networkManager: NetworkManagerProtocol) {
            self.networkManager = networkManager
        }

        func refreshBackup(auth: BackupServiceAuth, logger: PrefixedLogger) async throws {
            _ = try await networkManager.asyncRequest(.refreshBackup(auth: auth, logger: logger))
        }
    }
}

// MARK: -

private extension TSRequest {
    static func refreshBackup(auth: BackupServiceAuth, logger: PrefixedLogger) -> TSRequest {
        var request = TSRequest(
            url: URL(string: "v1/archives")!,
            method: "POST",
            parameters: [:],
            logger: logger,
        )
        request.auth = .backup(auth)
        return request
    }
}