// // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import LibSignalClient @objc public enum SignalServiceError: Int, Error { case obsoleteLinkedDevice } // MARK: - public protocol SignalServiceClient { func getAvailablePreKeys(for identity: OWSIdentity) -> Promise<(ecCount: Int, pqCount: Int)> /// If a username and password are both provided, those are used for the request's /// Authentication header. Otherwise, the default header is used (whatever's on /// TSAccountManager). func registerPreKeys( for identity: OWSIdentity, signedPreKeyRecord: SignalServiceKit.SignedPreKeyRecord?, preKeyRecords: [SignalServiceKit.PreKeyRecord]?, pqLastResortPreKeyRecord: KyberPreKeyRecord?, pqPreKeyRecords: [KyberPreKeyRecord]?, auth: ChatServiceAuth ) -> Promise func setCurrentSignedPreKey(_ signedPreKey: SignalServiceKit.SignedPreKeyRecord, for identity: OWSIdentity) -> Promise func requestUDSenderCertificate(uuidOnly: Bool) -> Promise func updatePrimaryDeviceAccountAttributes(authedAccount: AuthedAccount) async throws -> AccountAttributes func getAccountWhoAmI() -> Promise func requestStorageAuth(chatServiceAuth: ChatServiceAuth) -> Promise<(username: String, password: String)> func getRemoteConfig(auth: ChatServiceAuth) -> Promise // MARK: - Secondary Devices func updateSecondaryDeviceCapabilities(_ capabilities: AccountAttributes.Capabilities, authedAccount: AuthedAccount) async throws } extension SignalServiceClient { public func updatePrimaryDeviceAccountAttributes() async throws -> AccountAttributes { return try await updatePrimaryDeviceAccountAttributes(authedAccount: .implicit()) } public func updateSecondaryDeviceCapabilities(_ capabilities: AccountAttributes.Capabilities) async throws { try await updateSecondaryDeviceCapabilities(capabilities, authedAccount: .implicit()) } } // MARK: - public enum RemoteConfigItem { case isEnabled(Bool) case value(String) } public struct RemoteConfigResponse { public let items: [String: RemoteConfigItem] public let serverEpochTimeSeconds: UInt64? } // MARK: - /// Based on libsignal-service-java's PushServiceSocket class @objc public class SignalServiceRestClient: NSObject, SignalServiceClient, Dependencies { public static let shared = SignalServiceRestClient() // MARK: - Public public func getAvailablePreKeys(for identity: OWSIdentity) -> Promise<(ecCount: Int, pqCount: Int)> { let request = OWSRequestFactory.availablePreKeysCountRequest(for: identity) return firstly { networkManager.makePromise(request: request) }.map(on: DispatchQueue.global()) { response in guard let json = response.responseBodyJson else { throw OWSAssertionError("Missing or invalid JSON.") } guard let params = ParamParser(responseObject: json) else { throw OWSAssertionError("Missing or invalid response.") } let ecCount: Int = try params.required(key: "count") let pqCount: Int = try params.optional(key: "pqCount") ?? 0 return (ecCount, pqCount) } } public func registerPreKeys( for identity: OWSIdentity, signedPreKeyRecord: SignalServiceKit.SignedPreKeyRecord?, preKeyRecords: [SignalServiceKit.PreKeyRecord]?, pqLastResortPreKeyRecord: KyberPreKeyRecord?, pqPreKeyRecords: [KyberPreKeyRecord]?, auth: ChatServiceAuth ) -> Promise { let request = OWSRequestFactory.registerPrekeysRequest( identity: identity, signedPreKeyRecord: signedPreKeyRecord, prekeyRecords: preKeyRecords, pqLastResortPreKeyRecord: pqLastResortPreKeyRecord, pqPreKeyRecords: pqPreKeyRecords, auth: auth ) return networkManager.makePromise(request: request).asVoid() } public func setCurrentSignedPreKey(_ signedPreKey: SignalServiceKit.SignedPreKeyRecord, for identity: OWSIdentity) -> Promise { let request = OWSRequestFactory.registerSignedPrekeyRequest(for: identity, signedPreKey: signedPreKey) return networkManager.makePromise(request: request).asVoid() } public func requestUDSenderCertificate(uuidOnly: Bool) -> Promise { let request = OWSRequestFactory.udSenderCertificateRequest(uuidOnly: uuidOnly) return firstly { self.networkManager.makePromise(request: request) }.map(on: DispatchQueue.global()) { response in guard let json = response.responseBodyJson else { throw OWSUDError.invalidData(description: "Missing or invalid JSON") } guard let parser = ParamParser(responseObject: json) else { throw OWSUDError.invalidData(description: "Invalid sender certificate response") } return try parser.requiredBase64EncodedData(key: "certificate") } } public func updatePrimaryDeviceAccountAttributes(authedAccount: AuthedAccount) async throws -> AccountAttributes { guard DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isPrimaryDevice == true else { throw OWSAssertionError("only primary device should update account attributes") } let attributes = await self.databaseStorage.awaitableWrite { transaction in return AccountAttributes.generateForPrimaryDevice( fromDependencies: self, svr: DependenciesBridge.shared.svr, transaction: transaction ) } let request = AccountAttributesRequestFactory.updatePrimaryDeviceAttributesRequest(attributes) request.setAuth(authedAccount.chatServiceAuth) _ = try await networkManager.makePromise(request: request).awaitable() return attributes } public func getAccountWhoAmI() -> Promise { let request = WhoAmIRequestFactory.whoAmIRequest(auth: .implicit()) return firstly { networkManager.makePromise(request: request) }.map(on: DispatchQueue.global()) { response in guard let json = response.responseBodyData else { throw OWSAssertionError("Missing or invalid JSON.") } return try JSONDecoder().decode(WhoAmIRequestFactory.Responses.WhoAmI.self, from: json) } } public func requestStorageAuth(chatServiceAuth: ChatServiceAuth) -> Promise<(username: String, password: String)> { let request = OWSRequestFactory.storageAuthRequest() request.setAuth(chatServiceAuth) return firstly { networkManager.makePromise(request: request) }.map(on: DispatchQueue.global()) { response in guard let json = response.responseBodyJson else { throw OWSAssertionError("Missing or invalid JSON.") } guard let parser = ParamParser(responseObject: json) else { throw OWSAssertionError("Missing or invalid response.") } let username: String = try parser.required(key: "username") let password: String = try parser.required(key: "password") return (username: username, password: password) } } // yields a map of ["feature_name": isEnabled] public func getRemoteConfig(auth: ChatServiceAuth) -> Promise { let request = OWSRequestFactory.getRemoteConfigRequest() request.setAuth(auth) return firstly { networkManager.makePromise(request: request) }.map(on: DispatchQueue.global()) { response in guard let json = response.responseBodyJson else { throw OWSAssertionError("Missing or invalid JSON.") } guard let parser = ParamParser(responseObject: json) else { throw OWSAssertionError("Missing or invalid response.") } let config: [[String: Any]] = try parser.required(key: "config") let serverEpochTimeSeconds: UInt64? = try parser.optional(key: "serverEpochTime") let items: [String: RemoteConfigItem] = try config.reduce([:]) { accum, item in var accum = accum guard let itemParser = ParamParser(responseObject: item) else { throw OWSAssertionError("Missing or invalid remote config item.") } let name: String = try itemParser.required(key: "name") let isEnabled: Bool = try itemParser.required(key: "enabled") if let value: String = try itemParser.optional(key: "value") { accum[name] = .value(value) } else { accum[name] = .isEnabled(isEnabled) } return accum } return .init(items: items, serverEpochTimeSeconds: serverEpochTimeSeconds) } } // MARK: - Secondary Devices public func updateSecondaryDeviceCapabilities(_ capabilities: AccountAttributes.Capabilities, authedAccount: AuthedAccount) async throws { let request = AccountAttributesRequestFactory.updateLinkedDeviceCapabilitiesRequest( capabilities, tsAccountManager: DependenciesBridge.shared.tsAccountManager ) request.setAuth(authedAccount.chatServiceAuth) _ = try await networkManager.makePromise(request: request).awaitable() } }