// 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] = [] 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 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 { 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] { // 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() } } }