mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1429 lines
56 KiB
Swift
1429 lines
56 KiB
Swift
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<RequestWebViewResult, RequestWebViewError> {
|
|
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<RequestWebViewResult, RequestWebViewError> 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<RequestWebViewResult, RequestWebViewError> 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<RequestWebViewResult, RequestWebViewError> {
|
|
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<RequestWebViewResult, RequestWebViewError> 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<RequestWebViewResult, RequestWebViewError> 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<Never, KeepWebViewError>?
|
|
}
|
|
|
|
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<Never, KeepWebViewError> {
|
|
let signal = Signal<Never, KeepWebViewError> { subscriber in
|
|
let poll = Signal<Never, KeepWebViewError> { 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<Never, KeepWebViewError> = 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<RequestWebViewResult, RequestWebViewError> {
|
|
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<RequestWebViewResult, RequestWebViewError> 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<RequestWebViewResult, RequestWebViewError> 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<Never, KeepWebViewError>?
|
|
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<Never, SendWebViewDataError> {
|
|
return postbox.transaction { transaction -> Signal<Never, SendWebViewDataError> 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<RequestWebViewResult, RequestWebViewError> {
|
|
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<RequestWebViewResult, RequestWebViewError> 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<RequestWebViewResult, RequestWebViewError> 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<Bool, NoError> {
|
|
return postbox.transaction { transaction -> Signal<Bool, NoError> 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<Api.Bool, NoError> 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<Never, NoError> {
|
|
return postbox.transaction { transaction -> Signal<Never, NoError> 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<Api.Updates?, NoError> 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<String, InvokeBotCustomMethodError> {
|
|
let params = Api.DataJSON.dataJSON(data: params)
|
|
return postbox.transaction { transaction -> Signal<String, InvokeBotCustomMethodError> 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 TelegramSecureBotStorageState: Codable, Equatable {
|
|
public let uuid: String
|
|
|
|
public init(uuid: String) {
|
|
self.uuid = uuid
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
|
|
|
self.uuid = try container.decode(String.self, forKey: "uuid")
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: StringCodingKey.self)
|
|
|
|
try container.encode(self.uuid, forKey: "uuid")
|
|
}
|
|
}
|
|
|
|
func _internal_secureBotStorageUuid(account: Account) -> Signal<String, NoError> {
|
|
return account.postbox.transaction { transaction -> String in
|
|
if let current = transaction.getPreferencesEntry(key: PreferencesKeys.secureBotStorageState())?.get(TelegramSecureBotStorageState.self) {
|
|
return current.uuid
|
|
}
|
|
|
|
let uuid = "\(Int64.random(in: 0 ..< .max))"
|
|
transaction.setPreferencesEntry(key: PreferencesKeys.secureBotStorageState(), value: PreferencesEntry(TelegramSecureBotStorageState(uuid: uuid)))
|
|
return uuid
|
|
}
|
|
}
|
|
|
|
private let maxBotStorageSize = 5 * 1024 * 1024
|
|
public struct TelegramBotStorageState: Codable, Equatable {
|
|
public struct KeyValue: Codable, Equatable {
|
|
var key: String
|
|
var value: String
|
|
}
|
|
|
|
public var data: [String: String]
|
|
|
|
public init(
|
|
data: [String: String]
|
|
) {
|
|
self.data = data
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
|
|
|
let values = try container.decode([KeyValue].self, forKey: "data")
|
|
var data: [String: String] = [:]
|
|
for pair in values {
|
|
data[pair.key] = pair.value
|
|
}
|
|
self.data = data
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: StringCodingKey.self)
|
|
|
|
var values: [KeyValue] = []
|
|
for (key, value) in self.data {
|
|
values.append(KeyValue(key: key, value: value))
|
|
}
|
|
try container.encode(values, forKey: "data")
|
|
}
|
|
}
|
|
|
|
private func _internal_updateBotStorageState(account: Account, peerId: EnginePeer.Id, update: @escaping (TelegramBotStorageState?) -> TelegramBotStorageState) -> Signal<Never, BotStorageError> {
|
|
return account.postbox.transaction { transaction -> Signal<Never, BotStorageError> in
|
|
let previousState = transaction.getPreferencesEntry(key: PreferencesKeys.botStorageState(peerId: peerId))?.get(TelegramBotStorageState.self)
|
|
let updatedState = update(previousState)
|
|
|
|
var totalSize = 0
|
|
for (_, value) in updatedState.data {
|
|
totalSize += value.utf8.count
|
|
}
|
|
guard totalSize <= maxBotStorageSize else {
|
|
return .fail(.quotaExceeded)
|
|
}
|
|
|
|
transaction.setPreferencesEntry(key: PreferencesKeys.botStorageState(peerId: peerId), value: PreferencesEntry(updatedState))
|
|
return .never()
|
|
}
|
|
|> castError(BotStorageError.self)
|
|
|> switchToLatest
|
|
|> ignoreValues
|
|
}
|
|
|
|
public enum BotStorageError {
|
|
case quotaExceeded
|
|
}
|
|
|
|
func _internal_setBotStorageValue(account: Account, peerId: EnginePeer.Id, key: String, value: String?) -> Signal<Never, BotStorageError> {
|
|
return _internal_updateBotStorageState(account: account, peerId: peerId, update: { current in
|
|
var data = current?.data ?? [:]
|
|
if let value {
|
|
data[key] = value
|
|
} else {
|
|
data.removeValue(forKey: key)
|
|
}
|
|
return TelegramBotStorageState(data: data)
|
|
})
|
|
}
|
|
|
|
func _internal_clearBotStorage(account: Account, peerId: EnginePeer.Id) -> Signal<Never, BotStorageError> {
|
|
return _internal_updateBotStorageState(account: account, peerId: peerId, update: { _ in
|
|
return TelegramBotStorageState(data: [:])
|
|
})
|
|
}
|
|
|
|
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<Never, NoError> {
|
|
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<Set<EnginePeer.Id>, NoError> {
|
|
let viewKey: PostboxViewKey = PostboxViewKey.preferencesPrefix(keyPrefix: PreferencesKeys.botBiometricsStatePrefix())
|
|
return account.postbox.combinedView(keys: [viewKey])
|
|
|> map { views -> Set<EnginePeer.Id> in
|
|
guard let view = views.views[viewKey] as? PreferencesPrefixView else {
|
|
return Set()
|
|
}
|
|
|
|
var result = Set<EnginePeer.Id>()
|
|
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<Never, NoError> {
|
|
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<Never, NoError> in
|
|
return account.postbox.transaction { transaction -> Api.InputPeer? in
|
|
return transaction.getPeer(chatId).flatMap(apiInputPeer)
|
|
}
|
|
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
|
|
guard let inputPeer else {
|
|
return .complete()
|
|
}
|
|
return account.network.request(Api.functions.account.toggleConnectedBotPaused(peer: inputPeer, paused: isPaused ? .boolTrue : .boolFalse))
|
|
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
|
return .single(.boolFalse)
|
|
}
|
|
|> ignoreValues
|
|
}
|
|
}
|
|
}
|
|
|
|
func _internal_removeChatManagingBot(account: Account, chatId: EnginePeer.Id) -> Signal<Never, NoError> {
|
|
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
|
|
),
|
|
rights: connectedBot.rights
|
|
))
|
|
} else {
|
|
return current
|
|
}
|
|
})
|
|
}
|
|
|> mapToSignal { _ -> Signal<Never, NoError> in
|
|
return account.postbox.transaction { transaction -> Api.InputPeer? in
|
|
return transaction.getPeer(chatId).flatMap(apiInputPeer)
|
|
}
|
|
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
|
|
guard let inputPeer else {
|
|
return .complete()
|
|
}
|
|
return account.network.request(Api.functions.account.disablePeerConnectedBot(peer: inputPeer))
|
|
|> `catch` { _ -> Signal<Api.Bool, NoError> 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<String>()
|
|
var statePromise = Promise<State>()
|
|
|
|
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<Impl>
|
|
|
|
public var state: Signal<State, NoError> {
|
|
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<State>()
|
|
|
|
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<Impl>
|
|
|
|
public var state: Signal<State, NoError> {
|
|
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<Never, NoError> {
|
|
return account.postbox.transaction { transaction -> Api.InputUser? in
|
|
return transaction.getPeer(id).flatMap(apiInputUser)
|
|
}
|
|
|> mapToSignal { inputPeer -> Signal<Never, NoError> 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<Api.StarRefProgram?, NoError> in
|
|
return .single(nil)
|
|
}
|
|
|> mapToSignal { result -> Signal<Never, NoError> 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<Api.payments.ConnectedStarRefBots?, NoError> 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<Api.payments.SuggestedStarRefBots?, NoError> 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<EngineConnectedStarRefBotsContext.Item, ConnectStarRefBotError> {
|
|
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<EngineConnectedStarRefBotsContext.Item, ConnectStarRefBotError> 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<EngineConnectedStarRefBotsContext.Item, ConnectStarRefBotError> 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<EngineConnectedStarRefBotsContext.Item, ConnectStarRefBotError> 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<Never, ConnectStarRefBotError> {
|
|
return account.postbox.transaction { transaction -> Api.InputPeer? in
|
|
return transaction.getPeer(id).flatMap(apiInputPeer)
|
|
}
|
|
|> castError(ConnectStarRefBotError.self)
|
|
|> mapToSignal { inputPeer -> Signal<Never, ConnectStarRefBotError> 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<Never, ConnectStarRefBotError> 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<EngineConnectedStarRefBotsContext.Item?, NoError> {
|
|
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<EngineConnectedStarRefBotsContext.Item?, NoError> 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<Api.payments.ConnectedStarRefBots?, NoError> in
|
|
return .single(nil)
|
|
}
|
|
|> mapToSignal { result -> Signal<EngineConnectedStarRefBotsContext.Item?, NoError> 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<Api.messages.Chats?, NoError> 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
|
|
}
|
|
}
|
|
}
|