// // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import LibSignalClient public protocol MessageBackupRequestManager { func fetchBackupServiceAuth(localAci: Aci, auth: ChatServiceAuth) async throws -> MessageBackupServiceAuth func reserveBackupId(localAci: Aci, auth: ChatServiceAuth) async throws func registerBackupKeys(auth: MessageBackupServiceAuth) async throws func fetchBackupUploadForm(auth: MessageBackupServiceAuth) async throws -> Upload.Form func fetchBackupMediaAttachmentUploadForm(auth: MessageBackupServiceAuth) async throws -> Upload.Form func fetchBackupInfo(auth: MessageBackupServiceAuth) async throws -> MessageBackupRemoteInfo func refreshBackupInfo(auth: MessageBackupServiceAuth) async throws func fetchCDNReadCredentials(cdn: Int32, auth: MessageBackupServiceAuth) async throws -> CDNReadCredential // TODO: Batched backup media // TODO: Backup media // TODO: List media objects // TODO: Delete media objects // TODO: Redeem receipt } public struct MessageBackupRequestManagerImpl: MessageBackupRequestManager { private let db: DB private let messageBackupAuthCredentialManager: MessageBackupAuthCredentialManager private let messageBackupKeyMaterial: MessageBackupKeyMaterial private let networkManager: NetworkManager init( db: DB, messageBackupAuthCredentialManager: MessageBackupAuthCredentialManager, messageBackupKeyMaterial: MessageBackupKeyMaterial, networkManager: NetworkManager ) { self.db = db self.messageBackupAuthCredentialManager = messageBackupAuthCredentialManager self.messageBackupKeyMaterial = messageBackupKeyMaterial self.networkManager = networkManager } // MARK: - Reserve Backup /// Onetime request to reserve this backup ID. public func reserveBackupId(localAci: Aci, auth: ChatServiceAuth) async throws { let backupRequestContext = try db.read { tx in return try messageBackupKeyMaterial.backupAuthRequestContext(localAci: localAci, tx: tx) } let base64RequestContext = Data(backupRequestContext.getRequest().serialize()).base64EncodedString() let request = try OWSRequestFactory.reserveBackupId(backupId: base64RequestContext, auth: auth) _ = try await networkManager.makePromise( request: request, canUseWebSocket: false // TODO[Backups]: Switch this back to true when reg supports websockets ).awaitable() } // MARK: - Backup Auth /// Create a `MessageBackupAuthCredential` from local ACI and the current valid backup credential. This /// `MessageBackupAuthCredential` is used to authenticate all further `/v1/archive` operations. public func fetchBackupServiceAuth(localAci: Aci, auth: ChatServiceAuth) async throws -> MessageBackupServiceAuth { let (backupKey, privateKey) = try db.read { tx in let backupKey = try messageBackupKeyMaterial.backupID(localAci: localAci, tx: tx) let privateKey = try messageBackupKeyMaterial.backupPrivateKey(localAci: localAci, tx: tx) return (backupKey, privateKey) } let authCredential = try await messageBackupAuthCredentialManager.fetchBackupCredential( localAci: localAci, auth: auth ) return try MessageBackupServiceAuth(backupKey: backupKey, privateKey: privateKey, authCredential: authCredential) } // MARK: - Register Backup /// Onetime request to register the backup public key. public func registerBackupKeys(auth: MessageBackupServiceAuth) async throws { _ = try await executeBackupServiceRequest( auth: auth, requestFactory: OWSRequestFactory.backupSetPublicKeyRequest(auth:) ) } // MARK: - Upload Forms /// CDN upload form for uploading a backup public func fetchBackupUploadForm(auth: MessageBackupServiceAuth) async throws -> Upload.Form { return try await executeBackupService( auth: auth, requestFactory: OWSRequestFactory.backupUploadFormRequest(auth:) ) } /// CDN upload form for uploading backup media public func fetchBackupMediaAttachmentUploadForm(auth: MessageBackupServiceAuth) async throws -> Upload.Form { return try await executeBackupService( auth: auth, requestFactory: OWSRequestFactory.backupMediaUploadFormRequest(auth:) ) } // MARK: - Backup Info /// Fetch details about the current backup public func fetchBackupInfo(auth: MessageBackupServiceAuth) async throws -> MessageBackupRemoteInfo { return try await executeBackupService( auth: auth, requestFactory: OWSRequestFactory.backupInfoRequest(auth:) ) } /// Backup keep-alive request. If not called, the backup may be deleted after 30 days. public func refreshBackupInfo(auth: MessageBackupServiceAuth) async throws { _ = try await executeBackupServiceRequest( auth: auth, requestFactory: OWSRequestFactory.backupRefreshInfoRequest(auth:) ) } /// Delete the current backup public func deleteBackup(auth: MessageBackupServiceAuth) async throws { _ = try await executeBackupServiceRequest( auth: auth, requestFactory: OWSRequestFactory.deleteBackupRequest(auth:) ) } // MARK: - Media /// Retrieve credentials used for reading from the CDN public func fetchCDNReadCredentials(cdn: Int32, auth: MessageBackupServiceAuth) async throws -> CDNReadCredential { return try await executeBackupService( auth: auth, requestFactory: { OWSRequestFactory.fetchCDNCredentials(auth: $0, cdn: cdn) } ) } // MARK: - Private utility methods private func executeBackupServiceRequest( auth: MessageBackupServiceAuth, requestFactory: (MessageBackupServiceAuth) -> TSRequest ) async throws -> HTTPResponse { return try await networkManager.makePromise( request: requestFactory(auth), canUseWebSocket: false // TODO[Backups]: Switch this back to true when reg supports websockets ).awaitable() } private func executeBackupService( auth: MessageBackupServiceAuth, requestFactory: (MessageBackupServiceAuth) -> TSRequest ) async throws -> T { let response = try await executeBackupServiceRequest( auth: auth, requestFactory: requestFactory ) guard let bodyData = response.responseBodyData else { throw OWSAssertionError("Missing body data") } return try JSONDecoder().decode(T.self, from: bodyData) } } public struct CDNReadCredential: Decodable { let headers: [String: String] }