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.
		
		
		
		
		
			
		
			
				
	
	
		
			168 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			168 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import Foundation
 | |
| import GRDB
 | |
| import PromiseKit
 | |
| import SignalCoreKit
 | |
| 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)
 | |
|     
 | |
|     public static func run(
 | |
|         _ job: Job,
 | |
|         queue: DispatchQueue,
 | |
|         success: @escaping (Job, Bool) -> (),
 | |
|         failure: @escaping (Job, Error?, Bool) -> (),
 | |
|         deferred: @escaping (Job) -> ()
 | |
|     ) {
 | |
|         // Don't run when inactive or not in main app
 | |
|         guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else {
 | |
|             deferred(job) // Don't need to do anything if it's not the main app
 | |
|             return
 | |
|         }
 | |
|         
 | |
|         // We need to check a UIApplication setting which needs to run on the main thread so if we aren't on
 | |
|         // the main thread then swap to it
 | |
|         guard Thread.isMainThread else {
 | |
|             DispatchQueue.main.async {
 | |
|                 run(job, queue: queue, success: success, failure: failure, deferred: deferred)
 | |
|             }
 | |
|             return
 | |
|         }
 | |
|         
 | |
|         // Push tokens don't normally change while the app is launched, so you would assume checking once
 | |
|         // during launch is sufficient, but e.g. on iOS11, users who have disabled "Allow Notifications"
 | |
|         // and disabled "Background App Refresh" will not be able to obtain an APN token. Enabling those
 | |
|         // settings does not restart the app, so we check every activation for users who haven't yet
 | |
|         // registered.
 | |
|         //
 | |
|         // It's also possible for a device to successfully register for push notifications but fail to
 | |
|         // register with Session
 | |
|         //
 | |
|         // Due to the above we want to re-register at least once every ~12 hours to ensure users will
 | |
|         // continue to receive push notifications
 | |
|         //
 | |
|         // In addition to this if we are custom running the job (eg. by toggling the push notification
 | |
|         // setting) then we should run regardless of the other settings so users have a mechanism to force
 | |
|         // the registration to run
 | |
|         let lastPushNotificationSync: Date = UserDefaults.standard[.lastPushNotificationSync]
 | |
|             .defaulting(to: Date.distantPast)
 | |
|         
 | |
|         guard
 | |
|             job.behaviour == .runOnce ||
 | |
|             !UIApplication.shared.isRegisteredForRemoteNotifications ||
 | |
|             Date().timeIntervalSince(lastPushNotificationSync) >= SyncPushTokensJob.maxFrequency
 | |
|         else {
 | |
|             deferred(job) // Don't need to do anything if push notifications are already registered
 | |
|             return
 | |
|         }
 | |
|         
 | |
|         Logger.info("Re-registering for remote notifications.")
 | |
|         
 | |
|         // Perform device registration
 | |
|         PushRegistrationManager.shared.requestPushTokens()
 | |
|             .then(on: queue) { (pushToken: String, voipToken: String) -> Promise<Void> in
 | |
|                 let (promise, seal) = Promise<Void>.pending()
 | |
|                 
 | |
|                 SyncPushTokensJob.registerForPushNotifications(
 | |
|                     pushToken: pushToken,
 | |
|                     voipToken: voipToken,
 | |
|                     isForcedUpdate: true,
 | |
|                     success: { seal.fulfill(()) },
 | |
|                     failure: seal.reject
 | |
|                 )
 | |
|                 
 | |
|                 return promise
 | |
|                     .done(on: queue) { _ in
 | |
|                         Logger.warn("Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))")
 | |
| 
 | |
|                         Storage.shared.write { db in
 | |
|                             db[.lastRecordedPushToken] = pushToken
 | |
|                             db[.lastRecordedVoipToken] = voipToken
 | |
|                         }
 | |
|                     }
 | |
|             }
 | |
|             .ensure(on: queue) { success(job, false) }    // We want to complete this job regardless of success or failure
 | |
|             .retainUntilComplete()
 | |
|     }
 | |
|     
 | |
|     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 {
 | |
|     return OWSIsDebugBuild() ? string : "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]"
 | |
| }
 | |
| 
 | |
| extension SyncPushTokensJob {
 | |
|     fileprivate static func registerForPushNotifications(
 | |
|         pushToken: String,
 | |
|         voipToken: String,
 | |
|         isForcedUpdate: Bool,
 | |
|         success: @escaping () -> (),
 | |
|         failure: @escaping (Error) -> (),
 | |
|         remainingRetries: Int = 3
 | |
|     ) {
 | |
|         let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs]
 | |
|         let pushTokenAsData = Data(hex: pushToken)
 | |
|         let promise: Promise<Void> = (isUsingFullAPNs ?
 | |
|             PushNotificationAPI.register(
 | |
|                 with: pushTokenAsData,
 | |
|                 publicKey: getUserHexEncodedPublicKey(),
 | |
|                 isForcedUpdate: isForcedUpdate
 | |
|             ) :
 | |
|             PushNotificationAPI.unregister(pushTokenAsData)
 | |
|         )
 | |
|         
 | |
|         promise
 | |
|             .done { success() }
 | |
|             .catch { error in
 | |
|                 guard remainingRetries == 0 else {
 | |
|                     SyncPushTokensJob.registerForPushNotifications(
 | |
|                         pushToken: pushToken,
 | |
|                         voipToken: voipToken,
 | |
|                         isForcedUpdate: isForcedUpdate,
 | |
|                         success: success,
 | |
|                         failure: failure,
 | |
|                         remainingRetries: (remainingRetries - 1)
 | |
|                     )
 | |
|                     return
 | |
|                 }
 | |
|                 
 | |
|                 failure(error)
 | |
|             }
 | |
|             .retainUntilComplete()
 | |
|     }
 | |
| }
 |