//
// Copyright 2016 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
class SyncPushTokensJob: NSObject {
enum Mode {
case normal
case forceRotation
case rotateIfEligible
}
private let mode: Mode
init(mode: Mode) {
self.mode = mode
}
private static let hasUploadedTokensOnce = AtomicBool(false, lock: .sharedGlobal)
func run() async throws {
switch mode {
case .normal:
// Don't rotate.
return try await run(shouldRotateAPNSToken: false)
case .forceRotation:
// Always rotate
return try await run(shouldRotateAPNSToken: true)
case .rotateIfEligible:
let shouldRotate = SSKEnvironment.shared.databaseStorageRef.read { tx -> Bool in
return APNSRotationStore.canRotateAPNSToken(transaction: tx)
}
guard shouldRotate else {
// If we aren't rotating, no-op.
return
}
return try await run(shouldRotateAPNSToken: true)
}
}
typealias ApnRegistrationId = RegistrationRequestFactory.ApnRegistrationId
private func run(shouldRotateAPNSToken: Bool) async throws {
let regResult = try await AppEnvironment.shared.pushRegistrationManagerRef.requestPushTokens(forceRotation: shouldRotateAPNSToken)
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
if shouldRotateAPNSToken {
APNSRotationStore.didRotateAPNSToken(transaction: tx)
}
}
let pushToken = regResult.apnsToken
let reason: String
if SSKEnvironment.shared.preferencesRef.pushToken != pushToken {
reason = "changed"
} else if !Self.hasUploadedTokensOnce.get() {
reason = "launched"
} else {
return
}
Logger.info("uploading push token; reason: \(reason), pushToken: \(redact(pushToken))")
try await self.updatePushTokens(pushToken: pushToken)
await recordPushTokensLocally(pushToken: pushToken)
Self.hasUploadedTokensOnce.set(true)
}
class func run(mode: Mode = .normal) {
Task {
do {
try await SyncPushTokensJob(mode: mode).run()
} catch {
Logger.error("Error: \(error).")
}
}
}
private func recordPushTokensLocally(pushToken: String) async {
assert(!Thread.isMainThread)
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
if pushToken != SSKEnvironment.shared.preferencesRef.getPushToken(tx: tx) {
Logger.info("saved new push token: \(redact(pushToken))")
SSKEnvironment.shared.preferencesRef.setPushToken(pushToken, tx: tx)
}
}
}
// MARK: - Requests
private func updatePushTokens(pushToken: String) async throws {
return try await Retry.performWithBackoff(maxAttempts: 3) {
let request = OWSRequestFactory.registerForPushRequest(apnsToken: pushToken)
_ = try await SSKEnvironment.shared.networkManagerRef.asyncRequest(request)
}
}
}
private func redact(_ string: String?) -> String {
guard let string else { return "nil" }
#if DEBUG
return string
#else
return "\(string.prefix(2))…\(string.suffix(2))"
#endif
}