session-ios/Session/Utilities/BackgroundPoller.swift

131 lines
5.8 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import SessionSnodeKit
import SessionMessagingKit
import SessionUtilitiesKit
public final class BackgroundPoller {
private static var publishers: [AnyPublisher<Void, Error>] = []
public static var isValid: Bool = false
public static func poll(
completionHandler: @escaping (UIBackgroundFetchResult) -> Void,
using dependencies: Dependencies = Dependencies()
) {
Publishers
.MergeMany(
[pollForMessages(using: dependencies)]
.appending(contentsOf: pollForClosedGroupMessages(using: dependencies))
.appending(
contentsOf: Storage.shared
.read { db in
/// The default room promise creates an OpenGroup with an empty `roomToken` value, we
/// don't want to start a poller for this as the user hasn't actually joined a room
///
/// We also want to exclude any rooms which have failed to poll too many times in a row from
/// the background poll as they are likely to fail again
try OpenGroup
.select(.server)
.filter(
OpenGroup.Columns.roomToken != "" &&
OpenGroup.Columns.isActive &&
OpenGroup.Columns.pollFailureCount < OpenGroupAPI.Poller.maxRoomFailureCountForBackgroundPoll
)
.distinct()
.asRequest(of: String.self)
.fetchSet(db)
}
.defaulting(to: [])
.map { server -> AnyPublisher<Void, Error> in
let poller: OpenGroupAPI.Poller = OpenGroupAPI.Poller(for: server)
poller.stop()
return poller.poll(
calledFromBackgroundPoller: true,
isBackgroundPollerValid: { BackgroundPoller.isValid },
isPostCapabilitiesRetry: false,
using: dependencies
)
}
)
)
.subscribe(on: DispatchQueue.global(qos: .background), using: dependencies)
.receive(on: DispatchQueue.main, using: dependencies)
.collect()
.sinkUntilComplete(
receiveCompletion: { result in
// If we have already invalidated the timer then do nothing (we essentially timed out)
guard BackgroundPoller.isValid else { return }
switch result {
case .finished: completionHandler(.newData)
case .failure(let error):
SNLog("Background poll failed due to error: \(error)")
completionHandler(.failed)
}
}
)
}
private static func pollForMessages(
using dependencies: Dependencies
) -> AnyPublisher<Void, Error> {
let userPublicKey: String = getUserHexEncodedPublicKey()
return SnodeAPI.getSwarm(for: userPublicKey)
.tryFlatMapWithRandomSnode { snode -> AnyPublisher<[Message], Error> in
CurrentUserPoller.poll(
namespaces: CurrentUserPoller.namespaces,
from: snode,
for: userPublicKey,
calledFromBackgroundPoller: true,
isBackgroundPollValid: { BackgroundPoller.isValid },
using: dependencies
)
}
.map { _ in () }
.eraseToAnyPublisher()
}
private static func pollForClosedGroupMessages(
using dependencies: Dependencies
) -> [AnyPublisher<Void, Error>] {
// Fetch all closed groups (excluding any don't contain the current user as a
// GroupMemeber as the user is no longer a member of those)
return Storage.shared
.read { db in
try ClosedGroup
.select(.threadId)
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db))
)
.asRequest(of: String.self)
.fetchAll(db)
}
.defaulting(to: [])
.map { groupPublicKey in
SnodeAPI.getSwarm(for: groupPublicKey)
.tryFlatMap { swarm -> AnyPublisher<[Message], Error> in
guard let snode: Snode = swarm.randomElement() else {
throw OnionRequestAPIError.insufficientSnodes
}
return ClosedGroupPoller.poll(
namespaces: ClosedGroupPoller.namespaces,
from: snode,
for: groupPublicKey,
calledFromBackgroundPoller: true,
isBackgroundPollValid: { BackgroundPoller.isValid },
using: dependencies
)
}
.map { _ in () }
.eraseToAnyPublisher()
}
}
}