TM-SGNL-iOS/SignalServiceKit/Network/API/Requests/AccountAttributes/AccountAttributesUpdaterImpl.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

231 lines
9.3 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public class AccountAttributesUpdaterImpl: AccountAttributesUpdater {
private let accountAttributesGenerator: AccountAttributesGenerator
private let appReadiness: AppReadiness
private let appVersion: AppVersion
private let dateProvider: DateProvider
private let db: any DB
private let kvStore: KeyValueStore
private let networkManager: NetworkManager
private let profileManager: ProfileManager
private let schedulers: Schedulers
private let svrLocalStorage: SVRLocalStorage
private let syncManager: SyncManagerProtocol
private let tsAccountManager: TSAccountManager
public init(
accountAttributesGenerator: AccountAttributesGenerator,
appReadiness: AppReadiness,
appVersion: AppVersion,
dateProvider: @escaping DateProvider,
db: any DB,
networkManager: NetworkManager,
profileManager: ProfileManager,
schedulers: Schedulers,
svrLocalStorage: SVRLocalStorage,
syncManager: SyncManagerProtocol,
tsAccountManager: TSAccountManager
) {
self.accountAttributesGenerator = accountAttributesGenerator
self.appReadiness = appReadiness
self.appVersion = appVersion
self.dateProvider = dateProvider
self.db = db
self.kvStore = KeyValueStore(collection: "AccountAttributesUpdater")
self.networkManager = networkManager
self.profileManager = profileManager
self.schedulers = schedulers
self.svrLocalStorage = svrLocalStorage
self.syncManager = syncManager
self.tsAccountManager = tsAccountManager
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
Task {
try await self.updateAccountAttributesIfNecessaryAttempt(authedAccount: .implicit())
}
}
NotificationCenter.default.addObserver(
self,
selector: #selector(reachabilityDidChange),
name: .reachabilityChanged,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc
private func reachabilityDidChange() {
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
Task {
try await self.updateAccountAttributesIfNecessaryAttempt(authedAccount: .implicit())
}
}
}
public func updateAccountAttributes(authedAccount: AuthedAccount) async throws {
await db.awaitableWrite { tx in
self.kvStore.setDate(self.dateProvider(), key: Keys.latestUpdateRequestDate, transaction: tx)
}
try await self.updateAccountAttributesIfNecessaryAttempt(authedAccount: authedAccount)
}
public func scheduleAccountAttributesUpdate(authedAccount: AuthedAccount, tx: DBWriteTransaction) {
self.kvStore.setDate(self.dateProvider(), key: Keys.latestUpdateRequestDate, transaction: tx)
tx.addAsyncCompletion(on: schedulers.global()) {
Task {
try? await self.updateAccountAttributesIfNecessaryAttempt(authedAccount: authedAccount)
}
}
}
// Performs a single attempt to update the account attributes.
//
// We need to update our account attributes in a variety of scenarios:
//
// * Every time the user upgrades to a new version.
// * Whenever the device capabilities change.
// This is useful during development and internal testing when
// moving between builds with different device capabilities.
// * Whenever another component of the system requests an attribute,
// update e.g. during registration, after rotating the profile key, etc.
//
// The client will retry failed attempts:
//
// * On launch.
// * When reachability changes.
private func updateAccountAttributesIfNecessaryAttempt(authedAccount: AuthedAccount) async throws {
guard appReadiness.isAppReady else {
Logger.info("Aborting; app is not ready.")
return
}
let currentAppVersion = appVersion.currentAppVersion
enum ShouldUpdate {
case no
case yes(
currentDeviceCapabilities: AccountAttributes.Capabilities,
lastAttributeRequestDate: Date?,
registrationState: TSRegistrationState
)
}
let shouldUpdate = db.read { tx -> ShouldUpdate in
let registrationState = self.tsAccountManager.registrationState(tx: tx)
let isRegistered = registrationState.isRegistered
guard isRegistered else {
return .no
}
// has non-nil value if isRegistered is true.
let hasBackedUpMasterKey = self.svrLocalStorage.getIsMasterKeyBackedUp(tx)
let currentDeviceCapabilities = AccountAttributes.Capabilities(hasSVRBackups: hasBackedUpMasterKey)
// Check if there's been a request for an attributes update.
let lastAttributeRequestDate = self.kvStore.getDate(Keys.latestUpdateRequestDate, transaction: tx)
if lastAttributeRequestDate != nil {
return .yes(
currentDeviceCapabilities: currentDeviceCapabilities,
lastAttributeRequestDate: lastAttributeRequestDate,
registrationState: registrationState
)
}
// Check if device capabilities have changed.
let lastUpdateDeviceCapabilities = self.kvStore.getDictionary(
Keys.lastUpdateDeviceCapabilities,
keyClass: NSString.self,
objectClass: NSNumber.self,
transaction: tx
) as [String: NSNumber]?
if lastUpdateDeviceCapabilities != currentDeviceCapabilities.requestParameters {
return .yes(
currentDeviceCapabilities: currentDeviceCapabilities,
lastAttributeRequestDate: lastAttributeRequestDate,
registrationState: registrationState
)
}
// Check if the app version has changed.
let lastUpdateAppVersion = self.kvStore.getString(Keys.lastUpdateAppVersion, transaction: tx)
if lastUpdateAppVersion != currentAppVersion {
return .yes(
currentDeviceCapabilities: currentDeviceCapabilities,
lastAttributeRequestDate: lastAttributeRequestDate,
registrationState: registrationState
)
}
return .no
}
switch shouldUpdate {
case .no:
return
case let .yes(currentDeviceCapabilities, lastAttributeRequestDate, registrationState):
Logger.info("Updating account attributes.")
let reportedDeviceCapabilities: AccountAttributes.Capabilities
if registrationState.isPrimaryDevice == true {
let attributes = await db.awaitableWrite { tx in
accountAttributesGenerator.generateForPrimary(tx: tx)
}
let request = AccountAttributesRequestFactory(
tsAccountManager: tsAccountManager
).updatePrimaryDeviceAttributesRequest(
attributes,
auth: authedAccount.chatServiceAuth
)
_ = try await networkManager.asyncRequest(request)
reportedDeviceCapabilities = attributes.capabilities
} else {
let request = AccountAttributesRequestFactory(
tsAccountManager: tsAccountManager
).updateLinkedDeviceCapabilitiesRequest(
currentDeviceCapabilities,
auth: authedAccount.chatServiceAuth
)
_ = try await networkManager.asyncRequest(request)
reportedDeviceCapabilities = currentDeviceCapabilities
}
// Kick off an async profile fetch (not awaited)
_ = profileManager.fetchLocalUsersProfile(authedAccount: authedAccount)
await db.awaitableWrite { tx in
self.kvStore.setString(currentAppVersion, key: Keys.lastUpdateAppVersion, transaction: tx)
self.kvStore.setObject(reportedDeviceCapabilities.requestParameters, key: Keys.lastUpdateDeviceCapabilities, transaction: tx)
// Clear the update request unless a new update has been requested
// while this update was in flight.
if
let lastAttributeRequestDate,
lastAttributeRequestDate == self.kvStore.getDate(Keys.latestUpdateRequestDate, transaction: tx)
{
self.kvStore.removeValue(forKey: Keys.latestUpdateRequestDate, transaction: tx)
}
}
// Primary devices should sync their configuration whenever they
// update their account attributes.
if registrationState.isRegisteredPrimaryDevice {
self.syncManager.sendConfigurationSyncMessage()
}
}
}
private enum Keys {
static let latestUpdateRequestDate = "latestUpdateRequestDate"
static let lastUpdateDeviceCapabilities = "lastUpdateDeviceCapabilities"
static let lastUpdateAppVersion = "lastUpdateAppVersion"
}
}