diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 151cc0e0b..c9418248d 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -142,7 +142,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv if OWSIdentityManager.shared().identityKeyPair() != nil { let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.startPollerIfNeeded() - appDelegate.startClosedGroupPollerIfNeeded() + appDelegate.startClosedGroupPoller() appDelegate.startOpenGroupPollersIfNeeded() // Do this only if we created a new Session ID, or if we already received the initial configuration message if UserDefaults.standard[.hasSyncedInitialConfiguration] { diff --git a/Session/Meta/AppDelegate.h b/Session/Meta/AppDelegate.h index ac88f3f32..76cf25ce5 100644 --- a/Session/Meta/AppDelegate.h +++ b/Session/Meta/AppDelegate.h @@ -10,8 +10,6 @@ extern NSString *const AppDelegateStoryboardMain; - (void)startPollerIfNeeded; - (void)stopPoller; -- (void)startClosedGroupPollerIfNeeded; -- (void)stopClosedGroupPoller; - (void)startOpenGroupPollersIfNeeded; - (void)stopOpenGroupPollers; diff --git a/Session/Meta/AppDelegate.m b/Session/Meta/AppDelegate.m index d8193af9b..83ca336d8 100644 --- a/Session/Meta/AppDelegate.m +++ b/Session/Meta/AppDelegate.m @@ -48,7 +48,6 @@ static NSTimeInterval launchStartedAt; @property (nonatomic) BOOL areVersionMigrationsComplete; @property (nonatomic) BOOL didAppLaunchFail; @property (nonatomic) LKPoller *poller; -@property (nonatomic) LKClosedGroupPoller *closedGroupPoller; @end @@ -414,7 +413,7 @@ static NSTimeInterval launchStartedAt; [[SNSnodeAPI getSnodePool] retainUntilComplete]; [self startPollerIfNeeded]; - [self startClosedGroupPollerIfNeeded]; + [self startClosedGroupPoller]; [self startOpenGroupPollersIfNeeded]; if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) { @@ -564,7 +563,7 @@ static NSTimeInterval launchStartedAt; [self.readReceiptManager setAreReadReceiptsEnabled:YES]; [self startPollerIfNeeded]; - [self startClosedGroupPollerIfNeeded]; + [self startClosedGroupPoller]; [self startOpenGroupPollersIfNeeded]; } } @@ -727,19 +726,6 @@ static NSTimeInterval launchStartedAt; - (void)stopPoller { [self.poller stop]; } -- (void)startClosedGroupPollerIfNeeded -{ - if (self.closedGroupPoller == nil) { - NSString *userPublicKey = [SNGeneralUtilities getUserPublicKey]; - if (userPublicKey != nil) { - self.closedGroupPoller = [[LKClosedGroupPoller alloc] init]; - } - } - [self.closedGroupPoller startIfNeeded]; -} - -- (void)stopClosedGroupPoller { [self.closedGroupPoller stop]; } - - (void)startOpenGroupPollersIfNeeded { [SNOpenGroupManager.shared startPolling]; diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index b2b65d728..56e4100b2 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -31,4 +31,13 @@ extension AppDelegate { } return promise } + + @objc func startClosedGroupPoller() { + guard OWSIdentityManager.shared().identityKeyPair() != nil else { return } + ClosedGroupPoller.shared.start() + } + + @objc func stopClosedGroupPoller() { + ClosedGroupPoller.shared.stop() + } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 4bf04e96f..c37be5df7 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -408,6 +408,8 @@ extension MessageReceiver { Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: groupPublicKey, using: transaction) // Store the formation timestamp Storage.shared.setClosedGroupFormationTimestamp(to: messageSentTimestamp, for: groupPublicKey, using: transaction) + // Start polling + ClosedGroupPoller.shared.startPolling(for: groupPublicKey) // Notify the PN server let _ = PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: getUserHexEncodedPublicKey()) } @@ -539,6 +541,7 @@ extension MessageReceiver { if wasCurrentUserRemoved { Storage.shared.removeClosedGroupPublicKey(groupPublicKey, using: transaction) Storage.shared.removeAllClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction) + ClosedGroupPoller.shared.stopPolling(for: groupPublicKey) let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) } let storage = SNMessagingKitConfiguration.shared.storage diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift index 2fac0e5ea..c570b7278 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift @@ -43,6 +43,8 @@ extension MessageSender { // Notify the user let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupCreated) infoMessage.save(with: transaction) + // Start polling + ClosedGroupPoller.shared.startPolling(for: groupPublicKey) // Return return when(fulfilled: promises).map2 { thread } } @@ -272,6 +274,7 @@ extension MessageSender { // Remove the group from the database and unsubscribe from PNs Storage.shared.removeAllClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction) Storage.shared.removeClosedGroupPublicKey(groupPublicKey, using: transaction) + ClosedGroupPoller.shared.stopPolling(for: groupPublicKey) let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) } }.map { _ in } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift index 111d57152..2765b541a 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift @@ -3,11 +3,12 @@ import PromiseKit @objc(LKClosedGroupPoller) public final class ClosedGroupPoller : NSObject { - private var isPolling = false - private var timer: Timer? + private var isPolling: [String:Bool] = [:] + private var timers: [String:Timer] = [:] // MARK: Settings - private static let pollInterval: TimeInterval = 3 + private static let minPollInterval: Double = 4 + private static let maxPollInterval: Double = 2 * 60 // MARK: Error private enum Error : LocalizedError { @@ -22,65 +23,117 @@ public final class ClosedGroupPoller : NSObject { } } + // MARK: Initialization + public static let shared = ClosedGroupPoller() + + private override init() { } + // MARK: Public API - @objc public func startIfNeeded() { + @objc public func start() { #if DEBUG assert(Thread.current.isMainThread) // Timers don't do well on background queues #endif - guard !isPolling else { return } - isPolling = true - timer = Timer.scheduledTimer(withTimeInterval: ClosedGroupPoller.pollInterval, repeats: true) { [weak self] _ in - let _ = self?.poll() - } + let storage = SNMessagingKitConfiguration.shared.storage + let allGroupPublicKeys = storage.getUserClosedGroupPublicKeys() + allGroupPublicKeys.forEach { startPolling(for: $0) } } - public func pollOnce() -> [Promise] { - guard !isPolling else { return [] } - isPolling = true - return poll() + public func startPolling(for groupPublicKey: String) { + guard !isPolling(for: groupPublicKey) else { return } + setUpPolling(for: groupPublicKey) + isPolling[groupPublicKey] = true } @objc public func stop() { - isPolling = false - timer?.invalidate() + let storage = SNMessagingKitConfiguration.shared.storage + let allGroupPublicKeys = storage.getUserClosedGroupPublicKeys() + allGroupPublicKeys.forEach { stopPolling(for: $0) } + } + + public func stopPolling(for groupPublicKey: String) { + timers[groupPublicKey]?.invalidate() + isPolling[groupPublicKey] = false } // MARK: Private API - private func poll() -> [Promise] { - guard isPolling else { return [] } - let publicKeys = Storage.shared.getUserClosedGroupPublicKeys() - return publicKeys.map { publicKey in - let promise = SnodeAPI.getSwarm(for: publicKey).then2 { [weak self] swarm -> Promise<[JSON]> in - // randomElement() uses the system's default random generator, which is cryptographically secure - guard let snode = swarm.randomElement() else { return Promise(error: Error.insufficientSnodes) } - guard let self = self, self.isPolling else { return Promise(error: Error.pollingCanceled) } - return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).map2 { - SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: publicKey) - } + private func setUpPolling(for groupPublicKey: String) { + poll(groupPublicKey).done2 { [weak self] _ in + DispatchQueue.main.async { // Timers don't do well on background queues + self?.pollRecursively(groupPublicKey) } - promise.done2 { [weak self] messages in - guard let self = self, self.isPolling else { return } - if !messages.isEmpty { - SNLog("Received \(messages.count) new message(s) in closed group with public key: \(publicKey).") + }.catch2 { [weak self] error in + // The error is logged in poll(_:) + DispatchQueue.main.async { // Timers don't do well on background queues + self?.pollRecursively(groupPublicKey) + } + } + } + + private func pollRecursively(_ groupPublicKey: String) { + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + guard isPolling(for: groupPublicKey), + let thread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID)) else { return } + // Get the received date of the last message in the thread. If we don't have any messages yet, pick some + // reasonable fake time interval to use instead. + let lastMessageDate = + (thread.numberOfInteractions() > 0) ? thread.lastInteraction.receivedAtDate() : Date().addingTimeInterval(-5 * 60) + let timeSinceLastMessage = Date().timeIntervalSince(lastMessageDate) + let minPollInterval = ClosedGroupPoller.minPollInterval + let limit: Double = 12 * 60 * 60 + let a = (ClosedGroupPoller.maxPollInterval - minPollInterval) / limit + let nextPollInterval = a * min(timeSinceLastMessage, limit) + minPollInterval + SNLog("Next poll interval for closed group with public key: \(groupPublicKey) is \(nextPollInterval) s.") + timers[groupPublicKey] = Timer.scheduledTimer(withTimeInterval: nextPollInterval, repeats: false) { [weak self] timer in + timer.invalidate() + self?.poll(groupPublicKey).done2 { _ in + DispatchQueue.main.async { // Timers don't do well on background queues + self?.pollRecursively(groupPublicKey) } - messages.forEach { json in - guard let envelope = SNProtoEnvelope.from(json) else { return } - do { - let data = try envelope.serializedData() - let job = MessageReceiveJob(data: data, isBackgroundPoll: false) - SNMessagingKitConfiguration.shared.storage.write { transaction in - SessionMessagingKit.JobQueue.shared.add(job, using: transaction) - } - } catch { - SNLog("Failed to deserialize envelope due to error: \(error).") - } + }.catch2 { error in + // The error is logged in poll(_:) + DispatchQueue.main.async { // Timers don't do well on background queues + self?.pollRecursively(groupPublicKey) } } - promise.catch2 { error in - SNLog("Polling failed for closed group with public key: \(publicKey) due to error: \(error).") + } + } + + private func poll(_ groupPublicKey: String) -> Promise { + guard isPolling(for: groupPublicKey) else { return Promise.value(()) } + let promise = SnodeAPI.getSwarm(for: groupPublicKey).then2 { [weak self] swarm -> Promise<[JSON]> in + // randomElement() uses the system's default random generator, which is cryptographically secure + guard let snode = swarm.randomElement() else { return Promise(error: Error.insufficientSnodes) } + guard let self = self, self.isPolling(for: groupPublicKey) else { return Promise(error: Error.pollingCanceled) } + return SnodeAPI.getRawMessages(from: snode, associatedWith: groupPublicKey).map2 { + SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: groupPublicKey) } - promise.retainUntilComplete() - return promise.map { _ in } } + promise.done2 { [weak self] rawMessages in + guard let self = self, self.isPolling(for: groupPublicKey) else { return } + if !rawMessages.isEmpty { + SNLog("Received \(rawMessages.count) new message(s) in closed group with public key: \(groupPublicKey).") + } + rawMessages.forEach { json in + guard let envelope = SNProtoEnvelope.from(json) else { return } + do { + let data = try envelope.serializedData() + let job = MessageReceiveJob(data: data, isBackgroundPoll: false) + SNMessagingKitConfiguration.shared.storage.write { transaction in + SessionMessagingKit.JobQueue.shared.add(job, using: transaction) + } + } catch { + SNLog("Failed to deserialize envelope due to error: \(error).") + } + } + } + promise.catch2 { error in + SNLog("Polling failed for closed group with public key: \(groupPublicKey) due to error: \(error).") + } + return promise.map { _ in } + } + + // MARK: Convenience + private func isPolling(for groupPublicKey: String) -> Bool { + return isPolling[groupPublicKey] ?? false } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 4dfc780a0..ff8dc35f7 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -18,7 +18,7 @@ public final class OpenGroupPoller : NSObject { } // MARK: Settings - private let pollForNewMessagesInterval: TimeInterval = 8 + private let pollForNewMessagesInterval: TimeInterval = 20 private let pollForDeletedMessagesInterval: TimeInterval = 30 private let pollForModeratorsInterval: TimeInterval = 10 * 60