mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
233 lines
11 KiB
Swift
233 lines
11 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import Combine
|
|
import GRDB
|
|
import SessionSnodeKit
|
|
import SessionMessagingKit
|
|
import SessionUtilitiesKit
|
|
|
|
public enum SyncPushTokensJob: JobExecutor {
|
|
public static let maxFailureCount: Int = -1
|
|
public static let requiresThreadId: Bool = false
|
|
public static let requiresInteractionId: Bool = false
|
|
private static let maxFrequency: TimeInterval = (12 * 60 * 60)
|
|
private static let maxRunFrequency: TimeInterval = 1
|
|
|
|
public static func run(
|
|
_ job: Job,
|
|
queue: DispatchQueue,
|
|
success: @escaping (Job, Bool, Dependencies) -> (),
|
|
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
|
|
deferred: @escaping (Job, Dependencies) -> (),
|
|
using dependencies: Dependencies = Dependencies()
|
|
) {
|
|
// Don't run when inactive or not in main app or if the user doesn't exist yet
|
|
guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else {
|
|
return deferred(job, dependencies) // Don't need to do anything if it's not the main app
|
|
}
|
|
guard Identity.userCompletedRequiredOnboarding() else {
|
|
Log.info("[SyncPushTokensJob] Deferred due to incomplete registration")
|
|
return deferred(job, dependencies)
|
|
}
|
|
|
|
/// Since this job can be dependant on network conditions it's possible for multiple jobs to run at the same time, while this shouldn't cause issues
|
|
/// it can result in multiple API calls getting made concurrently so to avoid this we defer the job as if the previous one was successful then the
|
|
/// `lastPushNotificationSync` value will prevent the subsequent call being made
|
|
guard
|
|
dependencies.jobRunner
|
|
.jobInfoFor(state: .running, variant: .syncPushTokens)
|
|
.filter({ key, info in key != job.id }) // Exclude this job
|
|
.isEmpty
|
|
else {
|
|
// Defer the job to run 'maxRunFrequency' from when this one ran (if we don't it'll try start
|
|
// it again immediately which is pointless)
|
|
let updatedJob: Job? = dependencies.storage.write { db in
|
|
try job
|
|
.with(nextRunTimestamp: dependencies.dateNow.timeIntervalSince1970 + maxRunFrequency)
|
|
.upserted(db)
|
|
}
|
|
|
|
Log.info("[SyncPushTokensJob] Deferred due to in progress job")
|
|
return deferred(updatedJob ?? job, dependencies)
|
|
}
|
|
|
|
// Determine if the device has 'Fast Mode' (APNS) enabled
|
|
let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs]
|
|
|
|
// If the job is running and 'Fast Mode' is disabled then we should try to unregister the existing
|
|
// token
|
|
guard isUsingFullAPNs else {
|
|
Just(dependencies.storage[.lastRecordedPushToken])
|
|
.setFailureType(to: Error.self)
|
|
.flatMap { lastRecordedPushToken -> AnyPublisher<Void, Error> in
|
|
// Tell the device to unregister for remote notifications (essentially try to invalidate
|
|
// the token if needed - we do this first to avoid wrid race conditions which could be
|
|
// triggered by the user immediately re-registering)
|
|
DispatchQueue.main.sync { UIApplication.shared.unregisterForRemoteNotifications() }
|
|
|
|
// Clear the old token
|
|
dependencies.storage.write(using: dependencies) { db in
|
|
db[.lastRecordedPushToken] = nil
|
|
}
|
|
|
|
// Unregister from our server
|
|
if let existingToken: String = lastRecordedPushToken {
|
|
Log.info("[SyncPushTokensJob] Unregister using last recorded push token: \(redact(existingToken))")
|
|
return PushNotificationAPI.unsubscribe(token: Data(hex: existingToken))
|
|
.map { _ in () }
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
Log.info("[SyncPushTokensJob] No previous token stored just triggering device unregister")
|
|
return Just(())
|
|
.setFailureType(to: Error.self)
|
|
.eraseToAnyPublisher()
|
|
}
|
|
.subscribe(on: queue, using: dependencies)
|
|
.sinkUntilComplete(
|
|
receiveCompletion: { result in
|
|
switch result {
|
|
case .finished: Log.info("[SyncPushTokensJob] Unregister Completed")
|
|
case .failure: Log.error("[SyncPushTokensJob] Unregister Failed")
|
|
}
|
|
|
|
// We want to complete this job regardless of success or failure
|
|
success(job, false, dependencies)
|
|
}
|
|
)
|
|
return
|
|
}
|
|
|
|
/// Perform device registration
|
|
///
|
|
/// **Note:** Apple's documentation states that we should re-register for notifications on every launch:
|
|
/// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1
|
|
Log.info("[SyncPushTokensJob] Re-registering for remote notifications")
|
|
PushRegistrationManager.shared.requestPushTokens()
|
|
.flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<(String, String)?, Error> in
|
|
Deferred {
|
|
Future<(String, String)?, Error> { resolver in
|
|
_ = LibSession.onPathsChanged(skipInitialCallbackIfEmpty: true) { paths, pathsChangedId in
|
|
// Only listen for the first callback
|
|
LibSession.removePathsChangedCallback(callbackId: pathsChangedId)
|
|
|
|
guard !paths.isEmpty else {
|
|
Log.info("[SyncPushTokensJob] OS subscription completed, skipping server subscription due to lack of paths")
|
|
return resolver(Result.success(nil))
|
|
}
|
|
|
|
resolver(Result.success((pushToken, voipToken)))
|
|
}
|
|
}
|
|
}.eraseToAnyPublisher()
|
|
}
|
|
.flatMap { (tokenInfo: (String, String)?) -> AnyPublisher<Void, Error> in
|
|
guard let (pushToken, voipToken): (String, String) = tokenInfo else {
|
|
return Just(())
|
|
.setFailureType(to: Error.self)
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
/// For our `subscribe` endpoint we only want to call it if:
|
|
/// • It's been longer than `SyncPushTokensJob.maxFrequency` since the last subscription;
|
|
/// • The token has changed; or
|
|
/// • We want to force an update
|
|
let timeSinceLastSubscription: TimeInterval = dependencies.dateNow
|
|
.timeIntervalSince(
|
|
dependencies.standardUserDefaults[.lastPushNotificationSync]
|
|
.defaulting(to: Date.distantPast)
|
|
)
|
|
let uploadOnlyIfStale: Bool? = {
|
|
guard
|
|
let detailsData: Data = job.details,
|
|
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
|
|
else { return nil }
|
|
|
|
return details.uploadOnlyIfStale
|
|
}()
|
|
|
|
guard
|
|
timeSinceLastSubscription >= SyncPushTokensJob.maxFrequency ||
|
|
dependencies.storage[.lastRecordedPushToken] != pushToken ||
|
|
uploadOnlyIfStale == false
|
|
else {
|
|
Log.info("[SyncPushTokensJob] OS subscription completed, skipping server subscription due to frequency")
|
|
return Just(())
|
|
.setFailureType(to: Error.self)
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
return PushNotificationAPI
|
|
.subscribe(
|
|
token: Data(hex: pushToken),
|
|
isForcedUpdate: true,
|
|
using: dependencies
|
|
)
|
|
.retry(3, using: dependencies)
|
|
.handleEvents(
|
|
receiveCompletion: { result in
|
|
switch result {
|
|
case .failure(let error):
|
|
Log.error("[SyncPushTokensJob] Failed to register due to error: \(error)")
|
|
|
|
case .finished:
|
|
Log.debug("[SyncPushTokensJob] Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))")
|
|
Log.info("[SyncPushTokensJob] Completed")
|
|
dependencies.standardUserDefaults[.lastPushNotificationSync] = dependencies.dateNow
|
|
|
|
dependencies.storage.write(using: dependencies) { db in
|
|
db[.lastRecordedPushToken] = pushToken
|
|
db[.lastRecordedVoipToken] = voipToken
|
|
}
|
|
}
|
|
}
|
|
)
|
|
.map { _ in () }
|
|
.eraseToAnyPublisher()
|
|
}
|
|
.subscribe(on: queue, using: dependencies)
|
|
.sinkUntilComplete(
|
|
// We want to complete this job regardless of success or failure
|
|
receiveCompletion: { _ in success(job, false, dependencies) }
|
|
)
|
|
}
|
|
|
|
public static func run(uploadOnlyIfStale: Bool) {
|
|
guard let job: Job = Job(
|
|
variant: .syncPushTokens,
|
|
behaviour: .runOnce,
|
|
details: SyncPushTokensJob.Details(
|
|
uploadOnlyIfStale: uploadOnlyIfStale
|
|
)
|
|
)
|
|
else { return }
|
|
|
|
SyncPushTokensJob.run(
|
|
job,
|
|
queue: DispatchQueue.global(qos: .default),
|
|
success: { _, _, _ in },
|
|
failure: { _, _, _, _ in },
|
|
deferred: { _, _ in }
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - SyncPushTokensJob.Details
|
|
|
|
extension SyncPushTokensJob {
|
|
public struct Details: Codable {
|
|
public let uploadOnlyIfStale: Bool
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
private func redact(_ string: String) -> String {
|
|
#if DEBUG
|
|
return "[ DEBUG_NOT_REDACTED \(string) ]" // stringlint:disable
|
|
#else
|
|
return "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]" // stringlint:disable
|
|
#endif
|
|
}
|