import Foundation import SwiftSignalKit import Postbox import TelegramApi import MtProtoKit public struct StarsRevenueStats: Equatable, Codable { private enum CodingKeys: String, CodingKey { case revenueGraph case balances case usdRate } static func key(peerId: PeerId) -> ValueBoxKey { let key = ValueBoxKey(length: 8 + 4) key.setInt64(0, value: peerId.toInt64()) return key } public struct Balances: Equatable, Codable { private enum CodingKeys: String, CodingKey { case currentBalance case availableBalance case overallRevenue case withdrawEnabled case nextWithdrawalTimestamp case currentBalanceStars case availableBalanceStars case overallRevenueStars } public let currentBalance: StarsAmount public let availableBalance: StarsAmount public let overallRevenue: StarsAmount public let withdrawEnabled: Bool public let nextWithdrawalTimestamp: Int32? public init( currentBalance: StarsAmount, availableBalance: StarsAmount, overallRevenue: StarsAmount, withdrawEnabled: Bool, nextWithdrawalTimestamp: Int32? ) { self.currentBalance = currentBalance self.availableBalance = availableBalance self.overallRevenue = overallRevenue self.withdrawEnabled = withdrawEnabled self.nextWithdrawalTimestamp = nextWithdrawalTimestamp } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) if let legacyCurrentBalance = try container.decodeIfPresent(Int64.self, forKey: .currentBalance) { self.currentBalance = StarsAmount(value: legacyCurrentBalance, nanos: 0) } else { self.currentBalance = try container.decode(StarsAmount.self, forKey: .currentBalanceStars) } if let legacyAvailableBalance = try container.decodeIfPresent(Int64.self, forKey: .availableBalance) { self.availableBalance = StarsAmount(value: legacyAvailableBalance, nanos: 0) } else { self.availableBalance = try container.decode(StarsAmount.self, forKey: .availableBalanceStars) } if let legacyOverallRevenue = try container.decodeIfPresent(Int64.self, forKey: .overallRevenue) { self.overallRevenue = StarsAmount(value: legacyOverallRevenue, nanos: 0) } else { self.overallRevenue = try container.decode(StarsAmount.self, forKey: .overallRevenueStars) } self.withdrawEnabled = try container.decode(Bool.self, forKey: .withdrawEnabled) self.nextWithdrawalTimestamp = try container.decodeIfPresent(Int32.self, forKey: .nextWithdrawalTimestamp) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.currentBalance, forKey: .currentBalanceStars) try container.encode(self.availableBalance, forKey: .availableBalanceStars) try container.encode(self.overallRevenue, forKey: .overallRevenueStars) try container.encode(self.withdrawEnabled, forKey: .withdrawEnabled) try container.encodeIfPresent(self.nextWithdrawalTimestamp, forKey: .nextWithdrawalTimestamp) } } public let revenueGraph: StatsGraph public let balances: Balances public let usdRate: Double init(revenueGraph: StatsGraph, balances: Balances, usdRate: Double) { self.revenueGraph = revenueGraph self.balances = balances self.usdRate = usdRate } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.revenueGraph = try container.decode(StatsGraph.self, forKey: .revenueGraph) self.balances = try container.decode(Balances.self, forKey: .balances) self.usdRate = try container.decode(Double.self, forKey: .usdRate) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.revenueGraph, forKey: .revenueGraph) try container.encode(self.balances, forKey: .balances) try container.encode(self.usdRate, forKey: .usdRate) } public static func == (lhs: StarsRevenueStats, rhs: StarsRevenueStats) -> Bool { if lhs.revenueGraph != rhs.revenueGraph { return false } if lhs.balances != rhs.balances { return false } if lhs.usdRate != rhs.usdRate { return false } return true } } public extension StarsRevenueStats { func withUpdated(balances: StarsRevenueStats.Balances) -> StarsRevenueStats { return StarsRevenueStats( revenueGraph: self.revenueGraph, balances: balances, usdRate: self.usdRate ) } } extension StarsRevenueStats { init(apiStarsRevenueStats: Api.payments.StarsRevenueStats, peerId: PeerId) { switch apiStarsRevenueStats { case let .starsRevenueStats(revenueGraph, balances, usdRate): self.init(revenueGraph: StatsGraph(apiStatsGraph: revenueGraph), balances: StarsRevenueStats.Balances(apiStarsRevenueStatus: balances), usdRate: usdRate) } } } extension StarsRevenueStats.Balances { init(apiStarsRevenueStatus: Api.StarsRevenueStatus) { switch apiStarsRevenueStatus { case let .starsRevenueStatus(flags, currentBalance, availableBalance, overallRevenue, nextWithdrawalAt): self.init(currentBalance: StarsAmount(apiAmount: currentBalance), availableBalance: StarsAmount(apiAmount: availableBalance), overallRevenue: StarsAmount(apiAmount: overallRevenue), withdrawEnabled: ((flags & (1 << 0)) != 0), nextWithdrawalTimestamp: nextWithdrawalAt) } } } public struct StarsRevenueStatsContextState: Equatable { public var stats: StarsRevenueStats? } private func requestStarsRevenueStats(postbox: Postbox, network: Network, peerId: PeerId, dark: Bool = false) -> Signal { return postbox.transaction { transaction -> Peer? in if let peer = transaction.getPeer(peerId) { return peer } return nil } |> mapToSignal { peer -> Signal in guard let peer, let inputPeer = apiInputPeer(peer) else { return .never() } var flags: Int32 = 0 if dark { flags |= (1 << 1) } return network.request(Api.functions.payments.getStarsRevenueStats(flags: flags, peer: inputPeer)) |> retryRequestIfNotFrozen |> map { result -> StarsRevenueStats? in guard let result else { return nil } return StarsRevenueStats(apiStarsRevenueStats: result, peerId: peerId) } } } private final class StarsRevenueStatsContextImpl { private let account: Account private let peerId: PeerId private var _state: StarsRevenueStatsContextState { didSet { if self._state != oldValue { self._statePromise.set(.single(self._state)) } } } private let _statePromise = Promise() var state: Signal { return self._statePromise.get() } private let disposable = MetaDisposable() private let updateDisposable = MetaDisposable() init(account: Account, peerId: PeerId) { assert(Queue.mainQueue().isCurrent()) self.account = account self.peerId = peerId self._state = StarsRevenueStatsContextState(stats: nil) self._statePromise.set(.single(self._state)) self.load() let _ = (account.postbox.transaction { transaction -> StarsRevenueStats? in return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStarsRevenueStats, key: StarsRevenueStats.key(peerId: peerId)))?.get(StarsRevenueStats.self) } |> deliverOnMainQueue).start(next: { [weak self] cachedResult in guard let self, let cachedResult else { return } self._state = StarsRevenueStatsContextState(stats: cachedResult) self._statePromise.set(.single(self._state)) }) } deinit { assert(Queue.mainQueue().isCurrent()) self.disposable.dispose() self.updateDisposable.dispose() } public func setUpdated(_ f: @escaping () -> Void) { let peerId = self.peerId self.updateDisposable.set((account.stateManager.updatedStarsRevenueStatus() |> deliverOnMainQueue).startStrict(next: { updates in if let _ = updates[peerId] { f() } })) } fileprivate func load() { assert(Queue.mainQueue().isCurrent()) let account = self.account let peerId = self.peerId let signal = requestStarsRevenueStats(postbox: self.account.postbox, network: self.account.network, peerId: self.peerId) |> mapToSignal { initial -> Signal in guard let initial else { return .single(nil) } return .single(initial) |> then( account.stateManager.updatedStarsRevenueStatus() |> mapToSignal { updates in if let balances = updates[peerId] { return .single(initial.withUpdated(balances: balances)) } return .complete() } ) } self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] stats in if let self { self._state = StarsRevenueStatsContextState(stats: stats) self._statePromise.set(.single(self._state)) if let stats { let _ = (self.account.postbox.transaction { transaction in if let entry = CodableEntry(stats) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStarsRevenueStats, key: StarsRevenueStats.key(peerId: peerId)), entry: entry) } }).start() } } })) } func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { if let token = graph.token { return requestGraph(postbox: self.account.postbox, network: self.account.network, peerId: self.peerId, token: token, x: x) } else { return .single(nil) } } } public final class StarsRevenueStatsContext { private let impl: QueueLocalObject public var state: Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.state.start(next: { value in subscriber.putNext(value) })) } return disposable } } public init(account: Account, peerId: PeerId) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { return StarsRevenueStatsContextImpl(account: account, peerId: peerId) }) } public func setUpdated(_ f: @escaping () -> Void) { self.impl.with { impl in impl.setUpdated(f) } } public func reload() { self.impl.with { impl in impl.load() } } public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.loadDetailedGraph(graph, x: x).start(next: { value in subscriber.putNext(value) subscriber.putCompletion() })) } return disposable } } } public enum RequestStarsRevenueWithdrawalError : Equatable { case generic case twoStepAuthMissing case twoStepAuthTooFresh(Int32) case authSessionTooFresh(Int32) case limitExceeded case requestPassword case invalidPassword case serverProvided(text: String) } func _internal_checkStarsRevenueWithdrawalAvailability(account: Account) -> Signal { return account.network.request(Api.functions.payments.getStarsRevenueWithdrawalUrl(peer: .inputPeerEmpty, stars: 0, password: .inputCheckPasswordEmpty)) |> mapError { error -> RequestStarsRevenueWithdrawalError in if error.errorDescription == "PASSWORD_HASH_INVALID" { return .requestPassword } else if error.errorDescription == "PASSWORD_MISSING" { return .twoStepAuthMissing } else if error.errorDescription.hasPrefix("PASSWORD_TOO_FRESH_") { let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "PASSWORD_TOO_FRESH_".count)...]) if let value = Int32(timeout) { return .twoStepAuthTooFresh(value) } } else if error.errorDescription.hasPrefix("SESSION_TOO_FRESH_") { let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "SESSION_TOO_FRESH_".count)...]) if let value = Int32(timeout) { return .authSessionTooFresh(value) } } return .generic } |> ignoreValues } func _internal_requestStarsRevenueWithdrawalUrl(account: Account, peerId: PeerId, amount: Int64, password: String) -> Signal { guard !password.isEmpty else { return .fail(.invalidPassword) } return account.postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } let checkPassword = _internal_twoStepAuthData(account.network) |> mapError { error -> RequestStarsRevenueWithdrawalError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else { return .generic } } |> mapToSignal { authData -> Signal in if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData { guard let kdfResult = passwordKDF(encryptionProvider: account.network.encryptionProvider, password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else { return .fail(.generic) } return .single(.inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1))) } else { return .fail(.twoStepAuthMissing) } } return checkPassword |> mapToSignal { password -> Signal in return account.network.request(Api.functions.payments.getStarsRevenueWithdrawalUrl(peer: inputPeer, stars: amount, password: password), automaticFloodWait: false) |> mapError { error -> RequestStarsRevenueWithdrawalError in if error.errorCode == 406 { return .serverProvided(text: error.errorDescription) } else if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else if error.errorDescription == "PASSWORD_HASH_INVALID" { return .invalidPassword } else if error.errorDescription == "PASSWORD_MISSING" { return .twoStepAuthMissing } else if error.errorDescription.hasPrefix("PASSWORD_TOO_FRESH_") { let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "PASSWORD_TOO_FRESH_".count)...]) if let value = Int32(timeout) { return .twoStepAuthTooFresh(value) } } else if error.errorDescription.hasPrefix("SESSION_TOO_FRESH_") { let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "SESSION_TOO_FRESH_".count)...]) if let value = Int32(timeout) { return .authSessionTooFresh(value) } } return .generic } |> map { result -> String in switch result { case let .starsRevenueWithdrawalUrl(url): return url } } } } |> mapError { _ -> RequestStarsRevenueWithdrawalError in } |> switchToLatest } func _internal_requestStarsRevenueAdsAccountlUrl(account: Account, peerId: EnginePeer.Id) -> Signal { return account.postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { return .single(nil) } return account.network.request(Api.functions.payments.getStarsRevenueAdsAccountUrl(peer: inputPeer)) |> map(Optional.init) |> `catch` { error -> Signal in return .single(nil) } |> map { result -> String? in guard let result else { return nil } switch result { case let .starsRevenueAdsAccountUrl(url): return url } } } |> switchToLatest }