import Foundation import Postbox import SwiftSignalKit import TelegramApi import MtProtoKit #if os(macOS) private let botWebViewPlatform = "macos" #else private let botWebViewPlatform = "ios" #endif public enum RequestSimpleWebViewSource : Equatable { case generic case inline(startParam: String?) case settings } func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { serializedThemeParams = .dataJSON(data: dataString) } return postbox.transaction { transaction -> Signal in guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { return .fail(.generic) } var flags: Int32 = 0 if let _ = serializedThemeParams { flags |= (1 << 0) } var startParam: String? = nil switch source { case let .inline(_startParam): startParam = _startParam flags |= (1 << 1) case .settings: flags |= (1 << 2) default: break } if let _ = url { flags |= (1 << 3) } return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: startParam, themeParams: serializedThemeParams, platform: botWebViewPlatform)) |> mapError { _ -> RequestWebViewError in return .generic } |> mapToSignal { result -> Signal in switch result { case let .webViewResultUrl(flags, queryId, url): var resultFlags: RequestWebViewResult.Flags = [] if (flags & (1 << 1)) != 0 { resultFlags.insert(.fullSize) } if (flags & (1 << 2)) != 0 { resultFlags.insert(.fullScreen) } return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } } |> castError(RequestWebViewError.self) |> switchToLatest } func _internal_requestMainWebView(postbox: Postbox, network: Network, peerId: PeerId, botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { serializedThemeParams = .dataJSON(data: dataString) } return postbox.transaction { transaction -> Signal in guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { return .fail(.generic) } guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } var flags: Int32 = 0 if let _ = serializedThemeParams { flags |= (1 << 0) } var startParam: String? = nil switch source { case let .inline(_startParam): startParam = _startParam flags |= (1 << 1) case .settings: flags |= (1 << 2) default: break } return network.request(Api.functions.messages.requestMainWebView(flags: flags, peer: inputPeer, bot: inputUser, startParam: startParam, themeParams: serializedThemeParams, platform: botWebViewPlatform)) |> mapError { _ -> RequestWebViewError in return .generic } |> mapToSignal { result -> Signal in switch result { case let .webViewResultUrl(flags, queryId, url): var resultFlags: RequestWebViewResult.Flags = [] if (flags & (1 << 1)) != 0 { resultFlags.insert(.fullSize) } if (flags & (1 << 2)) != 0 { resultFlags.insert(.fullScreen) } return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } } |> castError(RequestWebViewError.self) |> switchToLatest } public enum KeepWebViewError { case generic } public struct RequestWebViewResult { public struct Flags: OptionSet { public var rawValue: Int32 public init(rawValue: Int32) { self.rawValue = rawValue } public init() { self.rawValue = 0 } public static let fullSize = Flags(rawValue: 1 << 0) public static let fullScreen = Flags(rawValue: 1 << 1) } public let flags: Flags public let queryId: Int64? public let url: String public let keepAliveSignal: Signal? } public enum RequestWebViewError { case generic } private func keepWebViewSignal(network: Network, stateManager: AccountStateManager, flags: Int32, peer: Api.InputPeer, bot: Api.InputUser, queryId: Int64, replyToMessageId: MessageId?, threadId: Int64?, sendAs: Api.InputPeer?) -> Signal { let signal = Signal { subscriber in let poll = Signal { subscriber in var replyTo: Api.InputReplyTo? if let replyToMessageId = replyToMessageId { var replyFlags: Int32 = 0 if threadId != nil { replyFlags |= 1 << 0 } replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyToMessageId.id, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil) } let signal: Signal = network.request(Api.functions.messages.prolongWebView(flags: flags, peer: peer, bot: bot, queryId: queryId, replyTo: replyTo, sendAs: sendAs)) |> mapError { _ -> KeepWebViewError in return .generic } |> ignoreValues return signal.start(error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() }) } let keepAliveSignal = ( .complete() |> suspendAwareDelay(60.0, queue: Queue.concurrentDefaultQueue()) |> then (poll) ) |> restart let pollDisposable = keepAliveSignal.start(error: { error in subscriber.putError(error) }) let dismissDisposable = (stateManager.dismissBotWebViews |> filter { $0.contains(queryId) } |> take(1)).start(completed: { subscriber.putCompletion() }) let disposableSet = DisposableSet() disposableSet.add(pollDisposable) disposableSet.add(dismissDisposable) return disposableSet } return signal } func _internal_requestWebView(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, botId: PeerId, url: String?, payload: String?, themeParams: [String: Any]?, fromMenu: Bool, replyToMessageId: MessageId?, threadId: Int64?) -> Signal { var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { serializedThemeParams = .dataJSON(data: dataString) } return postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer), let bot = transaction.getPeer(botId), let inputBot = apiInputUser(bot) else { return .fail(.generic) } var flags: Int32 = 0 if let _ = url { flags |= (1 << 1) } if let _ = serializedThemeParams { flags |= (1 << 2) } if let _ = payload { flags |= (1 << 3) } if fromMenu { flags |= (1 << 4) } var replyTo: Api.InputReplyTo? if let replyToMessageId = replyToMessageId { flags |= (1 << 0) var replyFlags: Int32 = 0 if threadId != nil { replyFlags |= 1 << 0 } replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyToMessageId.id, topMsgId: threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil) } return network.request(Api.functions.messages.requestWebView(flags: flags, peer: inputPeer, bot: inputBot, url: url, startParam: payload, themeParams: serializedThemeParams, platform: botWebViewPlatform, replyTo: replyTo, sendAs: nil)) |> mapError { _ -> RequestWebViewError in return .generic } |> mapToSignal { result -> Signal in switch result { case let .webViewResultUrl(webViewFlags, queryId, url): var resultFlags: RequestWebViewResult.Flags = [] if (webViewFlags & (1 << 1)) != 0 { resultFlags.insert(.fullSize) } if (webViewFlags & (1 << 2)) != 0 { resultFlags.insert(.fullScreen) } let keepAlive: Signal? if let queryId { keepAlive = keepWebViewSignal(network: network, stateManager: stateManager, flags: flags, peer: inputPeer, bot: inputBot, queryId: queryId, replyToMessageId: replyToMessageId, threadId: threadId, sendAs: nil) } else { keepAlive = nil } return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: keepAlive)) } } } |> castError(RequestWebViewError.self) |> switchToLatest } public enum SendWebViewDataError { case generic } func _internal_sendWebViewData(postbox: Postbox, network: Network, stateManager: AccountStateManager, botId: PeerId, buttonText: String, data: String) -> Signal { return postbox.transaction { transaction -> Signal in guard let bot = transaction.getPeer(botId), let inputBot = apiInputUser(bot) else { return .fail(.generic) } return network.request(Api.functions.messages.sendWebViewData(bot: inputBot, randomId: Int64.random(in: Int64.min ... Int64.max), buttonText: buttonText, data: data)) |> mapError { _ -> SendWebViewDataError in return .generic } |> map { updates -> Api.Updates in stateManager.addUpdates(updates) return updates } |> ignoreValues } |> castError(SendWebViewDataError.self) |> switchToLatest } func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, fullscreen: Bool, allowWrite: Bool) -> Signal { var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { serializedThemeParams = .dataJSON(data: dataString) } return postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } let app: Api.InputBotApp switch appReference { case let .id(id, accessHash): app = .inputBotAppID(id: id, accessHash: accessHash) case let .shortName(peerId, shortName): guard let bot = transaction.getPeer(peerId), let inputBot = apiInputUser(bot) else { return .fail(.generic) } app = .inputBotAppShortName(botId: inputBot, shortName: shortName) } var flags: Int32 = 0 if let _ = serializedThemeParams { flags |= (1 << 2) } if let _ = payload { flags |= (1 << 1) } if allowWrite { flags |= (1 << 0) } if compact { flags |= (1 << 7) } if fullscreen { flags |= (1 << 8) } return network.request(Api.functions.messages.requestAppWebView(flags: flags, peer: inputPeer, app: app, startParam: payload, themeParams: serializedThemeParams, platform: botWebViewPlatform)) |> mapError { _ -> RequestWebViewError in return .generic } |> mapToSignal { result -> Signal in switch result { case let .webViewResultUrl(flags, queryId, url): var resultFlags: RequestWebViewResult.Flags = [] if (flags & (1 << 1)) != 0 { resultFlags.insert(.fullSize) } if (flags & (1 << 2)) != 0 { resultFlags.insert(.fullScreen) } return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } } |> castError(RequestWebViewError.self) |> switchToLatest } func _internal_canBotSendMessages(postbox: Postbox, network: Network, botId: PeerId) -> Signal { return postbox.transaction { transaction -> Signal in guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { return .single(false) } return network.request(Api.functions.bots.canSendMessage(bot: inputUser)) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> map { result -> Bool in if case .boolTrue = result { return true } else { return false } } } |> switchToLatest } func _internal_allowBotSendMessages(postbox: Postbox, network: Network, stateManager: AccountStateManager, botId: PeerId) -> Signal { return postbox.transaction { transaction -> Signal in guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { return .never() } return network.request(Api.functions.bots.allowSendMessage(bot: inputUser)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> map { updates -> Api.Updates? in if let updates = updates { stateManager.addUpdates(updates) } return updates } |> ignoreValues } |> switchToLatest } public enum InvokeBotCustomMethodError { case generic } func _internal_invokeBotCustomMethod(postbox: Postbox, network: Network, botId: PeerId, method: String, params: String) -> Signal { let params = Api.DataJSON.dataJSON(data: params) return postbox.transaction { transaction -> Signal in guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { return .fail(.generic) } return network.request(Api.functions.bots.invokeWebViewCustomMethod(bot: inputUser, customMethod: method, params: params)) |> mapError { _ -> InvokeBotCustomMethodError in return .generic } |> map { result -> String in if case let .dataJSON(data) = result { return data } else { return "" } } } |> castError(InvokeBotCustomMethodError.self) |> switchToLatest } public struct TelegramBotBiometricsState: Codable, Equatable { public struct OpaqueToken: Codable, Equatable { public let publicKey: Data public let data: Data public init(publicKey: Data, data: Data) { self.publicKey = publicKey self.data = data } } public var deviceId: Data public var accessRequested: Bool public var accessGranted: Bool public var opaqueToken: OpaqueToken? public static func create() -> TelegramBotBiometricsState { var deviceId = Data(count: 32) deviceId.withUnsafeMutableBytes { buffer -> Void in arc4random_buf(buffer.assumingMemoryBound(to: UInt8.self).baseAddress!, buffer.count) } return TelegramBotBiometricsState( deviceId: deviceId, accessRequested: false, accessGranted: false, opaqueToken: nil ) } public init(deviceId: Data, accessRequested: Bool, accessGranted: Bool, opaqueToken: OpaqueToken?) { self.deviceId = deviceId self.accessRequested = accessRequested self.accessGranted = accessGranted self.opaqueToken = opaqueToken } } func _internal_updateBotBiometricsState(account: Account, peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState?) -> TelegramBotBiometricsState) -> Signal { return account.postbox.transaction { transaction -> Void in let previousState = transaction.getPreferencesEntry(key: PreferencesKeys.botBiometricsState(peerId: peerId))?.get(TelegramBotBiometricsState.self) transaction.setPreferencesEntry(key: PreferencesKeys.botBiometricsState(peerId: peerId), value: PreferencesEntry(update(previousState))) } |> ignoreValues } func _internal_botsWithBiometricState(account: Account) -> Signal, NoError> { let viewKey: PostboxViewKey = PostboxViewKey.preferencesPrefix(keyPrefix: PreferencesKeys.botBiometricsStatePrefix()) return account.postbox.combinedView(keys: [viewKey]) |> map { views -> Set in guard let view = views.views[viewKey] as? PreferencesPrefixView else { return Set() } var result = Set() for (key, value) in view.values { guard let peerId = PreferencesKeys.extractBotBiometricsStatePeerId(key: key) else { continue } if value.get(TelegramBotBiometricsState.self) == nil { continue } result.insert(peerId) } return result } } func _internal_toggleChatManagingBotIsPaused(account: Account, chatId: EnginePeer.Id) -> Signal { return account.postbox.transaction { transaction -> Bool in var isPaused = false transaction.updatePeerCachedData(peerIds: Set([chatId]), update: { _, current in guard let current = current as? CachedUserData else { return current } if var peerStatusSettings = current.peerStatusSettings { if let managingBot = peerStatusSettings.managingBot { isPaused = !managingBot.isPaused peerStatusSettings.managingBot?.isPaused = isPaused if !isPaused { peerStatusSettings.managingBot?.canReply = true } } return current.withUpdatedPeerStatusSettings(peerStatusSettings) } else { return current } }) return isPaused } |> mapToSignal { isPaused -> Signal in return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(chatId).flatMap(apiInputPeer) } |> mapToSignal { inputPeer -> Signal in guard let inputPeer else { return .complete() } return account.network.request(Api.functions.account.toggleConnectedBotPaused(peer: inputPeer, paused: isPaused ? .boolTrue : .boolFalse)) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> ignoreValues } } } func _internal_removeChatManagingBot(account: Account, chatId: EnginePeer.Id) -> Signal { return account.postbox.transaction { transaction -> Void in transaction.updatePeerCachedData(peerIds: Set([chatId]), update: { _, current in guard let current = current as? CachedUserData else { return current } if var peerStatusSettings = current.peerStatusSettings { peerStatusSettings.managingBot = nil return current.withUpdatedPeerStatusSettings(peerStatusSettings) } else { return current } }) transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in guard let current = current as? CachedUserData else { return current } if let connectedBot = current.connectedBot { var additionalPeers = connectedBot.recipients.additionalPeers var excludePeers = connectedBot.recipients.excludePeers if connectedBot.recipients.exclude { additionalPeers.insert(chatId) } else { additionalPeers.remove(chatId) excludePeers.insert(chatId) } return current.withUpdatedConnectedBot(TelegramAccountConnectedBot( id: connectedBot.id, recipients: TelegramBusinessRecipients( categories: connectedBot.recipients.categories, additionalPeers: additionalPeers, excludePeers: excludePeers, exclude: connectedBot.recipients.exclude ), canReply: connectedBot.canReply )) } else { return current } }) } |> mapToSignal { _ -> Signal in return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(chatId).flatMap(apiInputPeer) } |> mapToSignal { inputPeer -> Signal in guard let inputPeer else { return .complete() } return account.network.request(Api.functions.account.disablePeerConnectedBot(peer: inputPeer)) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> ignoreValues } } } public func formatPermille(_ value: Int32) -> String { return formatPermille(Int(value)) } public func formatPermille(_ value: Int) -> String { if value % 10 == 0 { return "\(value / 10)" } else { return String(format: "%.1f", Double(value) / 10.0) } } public enum StarRefBotConnectionEvent { case add(peerId: EnginePeer.Id, item: EngineConnectedStarRefBotsContext.Item) case remove(peerId: EnginePeer.Id, url: String) } public final class EngineConnectedStarRefBotsContext { public final class Item: Equatable { public let peer: EnginePeer public let url: String public let timestamp: Int32 public let commissionPermille: Int32 public let durationMonths: Int32? public let participants: Int64 public let revenue: Int64 public init(peer: EnginePeer, url: String, timestamp: Int32, commissionPermille: Int32, durationMonths: Int32?, participants: Int64, revenue: Int64) { self.peer = peer self.url = url self.timestamp = timestamp self.commissionPermille = commissionPermille self.durationMonths = durationMonths self.participants = participants self.revenue = revenue } public static func ==(lhs: Item, rhs: Item) -> Bool { if lhs.peer != rhs.peer { return false } if lhs.url != rhs.url { return false } if lhs.timestamp != rhs.timestamp { return false } if lhs.commissionPermille != rhs.commissionPermille { return false } if lhs.durationMonths != rhs.durationMonths { return false } if lhs.participants != rhs.participants { return false } if lhs.revenue != rhs.revenue { return false } return true } } public struct State: Equatable { public struct Offset: Equatable { fileprivate var isInitial: Bool fileprivate var timestamp: Int32 fileprivate var link: String fileprivate init(isInitial: Bool, timestamp: Int32, link: String) { self.isInitial = isInitial self.timestamp = timestamp self.link = link } } public var items: [Item] public var totalCount: Int public var nextOffset: Offset? public var isLoaded: Bool public init(items: [Item], totalCount: Int, nextOffset: Offset?, isLoaded: Bool) { self.items = items self.totalCount = totalCount self.nextOffset = nextOffset self.isLoaded = isLoaded } } private final class Impl { let queue: Queue let account: Account let peerId: EnginePeer.Id var state: State var pendingRemoveItems = Set() var statePromise = Promise() var loadMoreDisposable: Disposable? var isLoadingMore: Bool = false var eventsDisposable: Disposable? init(queue: Queue, account: Account, peerId: EnginePeer.Id) { self.queue = queue self.account = account self.peerId = peerId self.state = State(items: [], totalCount: 0, nextOffset: State.Offset(isInitial: true, timestamp: 0, link: ""), isLoaded: false) self.updateState() self.loadMore() self.eventsDisposable = (account.stateManager.starRefBotConnectionEvents() |> deliverOn(self.queue)).startStrict(next: { [weak self] event in guard let self else { return } switch event { case let .add(peerId, item): if peerId == self.peerId { self.state.items.insert(item, at: 0) self.updateState() } case let .remove(peerId, url): if peerId == self.peerId { self.state.items.removeAll(where: { $0.url == url }) self.updateState() } } }) } deinit { assert(self.queue.isCurrent()) self.loadMoreDisposable?.dispose() self.eventsDisposable?.dispose() } func loadMore() { if self.isLoadingMore { return } guard let offset = self.state.nextOffset else { return } self.isLoadingMore = true var effectiveOffset: (timestamp: Int32, link: String)? if !offset.isInitial { effectiveOffset = (timestamp: offset.timestamp, link: offset.link) } self.loadMoreDisposable?.dispose() self.loadMoreDisposable = (_internal_requestConnectedStarRefBots(account: self.account, id: self.peerId, offset: effectiveOffset, limit: 100) |> deliverOn(self.queue)).startStrict(next: { [weak self] result in guard let self else { return } self.isLoadingMore = false self.state.isLoaded = true if let result, !result.items.isEmpty { for item in result.items { if !self.state.items.contains(where: { $0.url == item.url }) { self.state.items.append(item) } } if result.nextOffset != nil { self.state.totalCount = result.totalCount } else { self.state.totalCount = self.state.items.count } self.state.nextOffset = result.nextOffset.flatMap { value in return State.Offset(isInitial: false, timestamp: value.timestamp, link: value.link) } } else { self.state.totalCount = self.state.items.count self.state.nextOffset = nil } self.updateState() }) } private func updateState() { var state = self.state if !self.pendingRemoveItems.isEmpty { state.items = state.items.filter { item in return !self.pendingRemoveItems.contains(item.url) } } self.statePromise.set(.single(state)) } func remove(url: String) { self.pendingRemoveItems.insert(url) let _ = _internal_removeConnectedStarRefBot(account: self.account, id: self.peerId, link: url).startStandalone() self.updateState() } } private let queue: Queue private let impl: QueueLocalObject public var state: Signal { return self.impl.signalWith { impl, subscriber in return impl.statePromise.get().start(next: subscriber.putNext) } } init(account: Account, peerId: EnginePeer.Id) { let queue = Queue.mainQueue() self.queue = queue self.impl = QueueLocalObject(queue: queue, generate: { return Impl(queue: queue, account: account, peerId: peerId) }) } public func loadMore() { self.impl.with { impl in impl.loadMore() } } public func remove(url: String) { self.impl.with { impl in impl.remove(url: url) } } } public final class EngineSuggestedStarRefBotsContext { public final class Item: Equatable { public let peer: EnginePeer public let program: TelegramStarRefProgram public init(peer: EnginePeer, program: TelegramStarRefProgram) { self.peer = peer self.program = program } public static func ==(lhs: Item, rhs: Item) -> Bool { if lhs.peer != rhs.peer { return false } if lhs.program != rhs.program { return false } return true } } public struct State: Equatable { public var items: [Item] public var totalCount: Int public var nextOffset: String? public var isLoaded: Bool public init(items: [Item], totalCount: Int, nextOffset: String?, isLoaded: Bool) { self.items = items self.totalCount = totalCount self.nextOffset = nextOffset self.isLoaded = isLoaded } } public enum SortMode { case date case profitability case revenue } private final class Impl { let queue: Queue let account: Account let peerId: EnginePeer.Id let sortMode: SortMode var state: State var statePromise = Promise() var loadMoreDisposable: Disposable? var isLoadingMore: Bool = false init(queue: Queue, account: Account, peerId: EnginePeer.Id, sortMode: SortMode) { self.queue = queue self.account = account self.peerId = peerId self.sortMode = sortMode self.state = State(items: [], totalCount: 0, nextOffset: "", isLoaded: false) self.updateState() self.loadMore() } deinit { assert(self.queue.isCurrent()) self.loadMoreDisposable?.dispose() } func loadMore() { if self.isLoadingMore { return } guard let offset = self.state.nextOffset else { return } self.isLoadingMore = true self.loadMoreDisposable?.dispose() self.loadMoreDisposable = (_internal_requestSuggestedStarRefBots(account: self.account, id: self.peerId, sortMode: self.sortMode, offset: offset, limit: 100) |> deliverOn(self.queue)).startStrict(next: { [weak self] result in guard let self else { return } self.isLoadingMore = false self.state.isLoaded = true if let result, !result.items.isEmpty { for item in result.items { if !self.state.items.contains(where: { $0.peer.id == item.peer.id }) { self.state.items.append(item) } } if result.nextOffset != nil { self.state.totalCount = result.totalCount } else { self.state.totalCount = self.state.items.count } self.state.nextOffset = result.nextOffset } else { self.state.totalCount = self.state.items.count self.state.nextOffset = nil } self.updateState() }) } private func updateState() { self.statePromise.set(.single(self.state)) } } private let queue: Queue public let sortMode: SortMode private let impl: QueueLocalObject public var state: Signal { return self.impl.signalWith { impl, subscriber in return impl.statePromise.get().start(next: subscriber.putNext) } } init(account: Account, peerId: EnginePeer.Id, sortMode: SortMode) { let queue = Queue.mainQueue() self.queue = queue self.sortMode = sortMode self.impl = QueueLocalObject(queue: queue, generate: { return Impl(queue: queue, account: account, peerId: peerId, sortMode: sortMode) }) } public func loadMore() { self.impl.with { impl in impl.loadMore() } } } func _internal_updateStarRefProgram(account: Account, id: EnginePeer.Id, program: (commissionPermille: Int32, durationMonths: Int32?)?) -> Signal { return account.postbox.transaction { transaction -> Api.InputUser? in return transaction.getPeer(id).flatMap(apiInputUser) } |> mapToSignal { inputPeer -> Signal in guard let inputPeer else { return .complete() } var flags: Int32 = 0 if let program, program.durationMonths != nil { flags |= 1 << 0 } return account.network.request(Api.functions.bots.updateStarRefProgram( flags: flags, bot: inputPeer, commissionPermille: program?.commissionPermille ?? 0, durationMonths: program?.durationMonths )) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal in guard let result else { return .complete() } return account.postbox.transaction { transaction -> Void in transaction.updatePeerCachedData(peerIds: Set([id]), update: { _, current in guard var current = current as? CachedUserData else { return current ?? CachedUserData() } current = current.withUpdatedStarRefProgram(TelegramStarRefProgram(apiStarRefProgram: result)) return current }) } |> ignoreValues } } } fileprivate func _internal_requestConnectedStarRefBots(account: Account, id: EnginePeer.Id, offset: (timestamp: Int32, link: String)?, limit: Int) -> Signal<(items: [EngineConnectedStarRefBotsContext.Item], totalCount: Int, nextOffset: (timestamp: Int32, link: String)?)?, NoError> { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(id).flatMap(apiInputPeer) } |> mapToSignal { inputPeer -> Signal<(items: [EngineConnectedStarRefBotsContext.Item], totalCount: Int, nextOffset: (timestamp: Int32, link: String)?)?, NoError> in guard let inputPeer else { return .single(nil) } var flags: Int32 = 0 if offset != nil { flags |= 1 << 2 } return account.network.request(Api.functions.payments.getConnectedStarRefBots( flags: flags, peer: inputPeer, offsetDate: offset?.timestamp, offsetLink: offset?.link, limit: Int32(limit) )) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal<(items: [EngineConnectedStarRefBotsContext.Item], totalCount: Int, nextOffset: (timestamp: Int32, link: String)?)?, NoError> in guard let result else { return .single(nil) } return account.postbox.transaction { transaction -> (items: [EngineConnectedStarRefBotsContext.Item], totalCount: Int, nextOffset: (timestamp: Int32, link: String)?)? in switch result { case let .connectedStarRefBots(count, connectedBots, users): updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) var items: [EngineConnectedStarRefBotsContext.Item] = [] for connectedBot in connectedBots { switch connectedBot { case let .connectedBotStarRef(_, url, date, botId, commissionPermille, durationMonths, participants, revenue): guard let botPeer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))) else { continue } items.append(EngineConnectedStarRefBotsContext.Item( peer: EnginePeer(botPeer), url: url, timestamp: date, commissionPermille: commissionPermille, durationMonths: durationMonths, participants: participants, revenue: revenue )) } } var nextOffset: (timestamp: Int32, link: String)? if !connectedBots.isEmpty { nextOffset = items.last.flatMap { item in return (item.timestamp, item.url) } } return (items: items, totalCount: Int(count), nextOffset: nextOffset) } } } } } fileprivate func _internal_requestSuggestedStarRefBots(account: Account, id: EnginePeer.Id, sortMode: EngineSuggestedStarRefBotsContext.SortMode, offset: String?, limit: Int) -> Signal<(items: [EngineSuggestedStarRefBotsContext.Item], totalCount: Int, nextOffset: String?)?, NoError> { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(id).flatMap(apiInputPeer) } |> mapToSignal { inputPeer -> Signal<(items: [EngineSuggestedStarRefBotsContext.Item], totalCount: Int, nextOffset: String?)?, NoError> in guard let inputPeer else { return .single(nil) } var flags: Int32 = 0 switch sortMode { case .revenue: flags |= 1 << 0 case .date: flags |= 1 << 1 case .profitability: break } return account.network.request(Api.functions.payments.getSuggestedStarRefBots( flags: flags, peer: inputPeer, offset: offset ?? "", limit: Int32(limit) )) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal<(items: [EngineSuggestedStarRefBotsContext.Item], totalCount: Int, nextOffset: String?)?, NoError> in guard let result else { return .single(nil) } return account.postbox.transaction { transaction -> (items: [EngineSuggestedStarRefBotsContext.Item], totalCount: Int, nextOffset: String?)? in switch result { case let .suggestedStarRefBots(_, count, suggestedBots, users, nextOffset): updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) var items: [EngineSuggestedStarRefBotsContext.Item] = [] for starRefProgram in suggestedBots { let parsedProgram = TelegramStarRefProgram(apiStarRefProgram: starRefProgram) guard let botPeer = transaction.getPeer(parsedProgram.botId) else { continue } items.append(EngineSuggestedStarRefBotsContext.Item( peer: EnginePeer(botPeer), program: parsedProgram )) } return (items: items, totalCount: Int(count), nextOffset: nextOffset) } } } } } public enum ConnectStarRefBotError { case generic } func _internal_connectStarRefBot(account: Account, id: EnginePeer.Id, botId: EnginePeer.Id) -> Signal { return account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputUser?) in return ( transaction.getPeer(id).flatMap(apiInputPeer), transaction.getPeer(botId).flatMap(apiInputUser) ) } |> castError(ConnectStarRefBotError.self) |> mapToSignal { inputPeer, inputBotUser -> Signal in guard let inputPeer, let inputBotUser else { return .fail(.generic) } return account.network.request(Api.functions.payments.connectStarRefBot(peer: inputPeer, bot: inputBotUser)) |> mapError { _ -> ConnectStarRefBotError in return .generic } |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> EngineConnectedStarRefBotsContext.Item? in switch result { case let .connectedStarRefBots(_, connectedBots, users): updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) if let bot = connectedBots.first { switch bot { case let .connectedBotStarRef(_, url, date, botId, commissionPermille, durationMonths, participants, revenue): guard let botPeer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))) else { return nil } return EngineConnectedStarRefBotsContext.Item( peer: EnginePeer(botPeer), url: url, timestamp: date, commissionPermille: commissionPermille, durationMonths: durationMonths, participants: participants, revenue: revenue ) } } else { return nil } } } |> castError(ConnectStarRefBotError.self) |> mapToSignal { item -> Signal in if let item { account.stateManager.addStarRefBotConnectionEvent(event: .add(peerId: id, item: item)) return .single(item) } else { return .fail(.generic) } } } } } fileprivate func _internal_removeConnectedStarRefBot(account: Account, id: EnginePeer.Id, link: String) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(id).flatMap(apiInputPeer) } |> castError(ConnectStarRefBotError.self) |> mapToSignal { inputPeer -> Signal in guard let inputPeer else { return .fail(.generic) } var flags: Int32 = 0 flags |= 1 << 0 return account.network.request(Api.functions.payments.editConnectedStarRefBot(flags: flags, peer: inputPeer, link: link)) |> mapError { _ -> ConnectStarRefBotError in return .generic } |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Void in switch result { case let .connectedStarRefBots(_, connectedBots, users): updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) let _ = connectedBots } account.stateManager.addStarRefBotConnectionEvent(event: .remove(peerId: id, url: link)) } |> castError(ConnectStarRefBotError.self) |> ignoreValues } } } func _internal_getStarRefBotConnection(account: Account, id: EnginePeer.Id, targetId: EnginePeer.Id) -> Signal { return account.postbox.transaction { transaction -> (Api.InputUser?, Api.InputPeer?) in return ( transaction.getPeer(id).flatMap(apiInputUser), transaction.getPeer(targetId).flatMap(apiInputPeer) ) } |> mapToSignal { inputPeer, targetPeer -> Signal in guard let inputPeer, let targetPeer else { return .single(nil) } return account.network.request(Api.functions.payments.getConnectedStarRefBot(peer: targetPeer, bot: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal in guard let result else { return .single(nil) } return account.postbox.transaction { transaction -> EngineConnectedStarRefBotsContext.Item? in switch result { case let .connectedStarRefBots(_, connectedBots, users): updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users)) if let bot = connectedBots.first { switch bot { case let .connectedBotStarRef(flags, url, date, botId, commissionPermille, durationMonths, participants, revenue): let isRevoked = (flags & (1 << 1)) != 0 if isRevoked { return nil } guard let botPeer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))) else { return nil } return EngineConnectedStarRefBotsContext.Item( peer: EnginePeer(botPeer), url: url, timestamp: date, commissionPermille: commissionPermille, durationMonths: durationMonths, participants: participants, revenue: revenue ) } } else { return nil } } } } } } func _internal_getPossibleStarRefBotTargets(account: Account) -> Signal<[EnginePeer], NoError> { return combineLatest( account.network.request(Api.functions.bots.getAdminedBots()) |> `catch` { _ -> Signal<[Api.User], NoError> in return .single([]) }, account.network.request(Api.functions.channels.getAdminedPublicChannels(flags: 0)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } ) |> mapToSignal { apiBots, apiChannels -> Signal<[EnginePeer], NoError> in return account.postbox.transaction { transaction -> [EnginePeer] in var result: [EnginePeer] = [] if let peer = transaction.getPeer(account.peerId) { result.append(EnginePeer(peer)) } updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: apiBots)) for bot in apiBots { if let peer = transaction.getPeer(bot.peerId) { result.append(EnginePeer(peer)) } } if let apiChannels { switch apiChannels { case let .chats(chats), let .chatsSlice(_, chats): updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(chats: chats, users: [])) for chat in chats { if let peer = transaction.getPeer(chat.peerId) { result.append(EnginePeer(peer)) } } } } return result } } }