mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
3baeb981d9
Moved the JobRunner into SessionUtilitiesKit so it can be used by SessionSnodeKit Exposed a 'sharedLokiProject' value on UserDefaults to remove the hard-coded group name used everywhere Added "blocking" job support for 'OnLaunch' and 'OnActive' jobs to the JobRunner (will retry until it succeeds) Added the UpdateProfilePicture and RetrieveDefaultOpenGroupRooms jobs
174 lines
7.6 KiB
Swift
174 lines
7.6 KiB
Swift
import SessionSnodeKit
|
|
import PromiseKit
|
|
|
|
@objc(LKClosedGroupPoller)
|
|
public final class ClosedGroupPoller : NSObject {
|
|
private var isPolling: [String:Bool] = [:]
|
|
private var timers: [String:Timer] = [:]
|
|
private let internalQueue: DispatchQueue = DispatchQueue(label:"isPollingQueue")
|
|
|
|
// MARK: Settings
|
|
private static let minPollInterval: Double = 2
|
|
private static let maxPollInterval: Double = 30
|
|
|
|
// MARK: Error
|
|
private enum Error : LocalizedError {
|
|
case insufficientSnodes
|
|
case pollingCanceled
|
|
|
|
internal var errorDescription: String? {
|
|
switch self {
|
|
case .insufficientSnodes: return "No snodes left to poll."
|
|
case .pollingCanceled: return "Polling canceled."
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Initialization
|
|
public static let shared = ClosedGroupPoller()
|
|
|
|
private override init() { }
|
|
|
|
// MARK: Public API
|
|
@objc public func start() {
|
|
#if DEBUG
|
|
assert(Thread.current.isMainThread) // Timers don't do well on background queues
|
|
#endif
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
let allGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
|
|
allGroupPublicKeys.forEach { startPolling(for: $0) }
|
|
}
|
|
|
|
public func startPolling(for groupPublicKey: String) {
|
|
guard !isPolling(for: groupPublicKey) else { return }
|
|
// Might be a race condition that the setUpPolling finishes too soon,
|
|
// and the timer is not created, if we mark the group as is polling
|
|
// after setUpPolling. So the poller may not work, thus misses messages.
|
|
internalQueue.sync{ isPolling[groupPublicKey] = true }
|
|
setUpPolling(for: groupPublicKey)
|
|
}
|
|
|
|
@objc public func stop() {
|
|
let storage = SNMessagingKitConfiguration.shared.storage
|
|
let allGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
|
|
allGroupPublicKeys.forEach { stopPolling(for: $0) }
|
|
}
|
|
|
|
public func stopPolling(for groupPublicKey: String) {
|
|
internalQueue.sync{ isPolling[groupPublicKey] = false }
|
|
timers[groupPublicKey]?.invalidate()
|
|
}
|
|
|
|
// MARK: Private API
|
|
private func setUpPolling(for groupPublicKey: String) {
|
|
Threading.pollerQueue.async {
|
|
self.poll(groupPublicKey).done(on: Threading.pollerQueue) { [weak self] _ in
|
|
self?.pollRecursively(groupPublicKey)
|
|
}.catch(on: Threading.pollerQueue) { [weak self] error in
|
|
// The error is logged in poll(_:)
|
|
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.scheduledTimerOnMainThread(withTimeInterval: nextPollInterval, repeats: false) { [weak self] timer in
|
|
timer.invalidate()
|
|
Threading.pollerQueue.async {
|
|
self?.poll(groupPublicKey).done(on: Threading.pollerQueue) { _ in
|
|
self?.pollRecursively(groupPublicKey)
|
|
}.catch(on: Threading.pollerQueue) { error in
|
|
// The error is logged in poll(_:)
|
|
self?.pollRecursively(groupPublicKey)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func poll(_ groupPublicKey: String) -> Promise<Void> {
|
|
guard isPolling(for: groupPublicKey) else { return Promise.value(()) }
|
|
|
|
let promise = SnodeAPI.getSwarm(for: groupPublicKey)
|
|
.then2 { [weak self] swarm -> Promise<(Snode, [SnodeReceivedMessage])> 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 {
|
|
let messages: [SnodeReceivedMessage] = SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: groupPublicKey)
|
|
|
|
return (snode, messages)
|
|
}
|
|
}
|
|
|
|
promise.done2 { [weak self] snode, messages in
|
|
guard let self = self, self.isPolling(for: groupPublicKey) else { return }
|
|
|
|
if !messages.isEmpty {
|
|
SNLog("Received \(messages.count) new message(s) in closed group with public key: \(groupPublicKey).")
|
|
|
|
GRDBStorage.shared.write { db in
|
|
var jobDetailMessages: [MessageReceiveJob.Details.MessageInfo] = []
|
|
|
|
messages.forEach { message in
|
|
guard let envelope = SNProtoEnvelope.from(message) else { return }
|
|
|
|
do {
|
|
jobDetailMessages.append(
|
|
MessageReceiveJob.Details.MessageInfo(
|
|
data: try envelope.serializedData(),
|
|
serverHash: message.info.hash
|
|
)
|
|
)
|
|
|
|
// Persist the received message after the MessageReceiveJob is created
|
|
_ = try message.info.saved(db)
|
|
}
|
|
catch {
|
|
SNLog("Failed to deserialize envelope due to error: \(error).")
|
|
}
|
|
}
|
|
|
|
JobRunner.add(
|
|
db,
|
|
job: Job(
|
|
variant: .messageReceive,
|
|
behaviour: .runOnce,
|
|
threadId: groupPublicKey,
|
|
details: MessageReceiveJob.Details(
|
|
messages: jobDetailMessages,
|
|
isBackgroundPoll: false
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
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 internalQueue.sync{ isPolling[groupPublicKey] ?? false }
|
|
}
|
|
}
|