Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,228 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
import MtProtoKit
public struct MessageStats: Equatable {
public let views: Int
public let forwards: Int
public let reactions: Int
public let interactionsGraph: StatsGraph
public let interactionsGraphDelta: Int64
public let reactionsGraph: StatsGraph
init(views: Int, forwards: Int, reactions: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) {
self.views = views
self.forwards = forwards
self.reactions = reactions
self.interactionsGraph = interactionsGraph
self.interactionsGraphDelta = interactionsGraphDelta
self.reactionsGraph = reactionsGraph
}
public static func == (lhs: MessageStats, rhs: MessageStats) -> Bool {
if lhs.views != rhs.views {
return false
}
if lhs.forwards != rhs.forwards {
return false
}
if lhs.reactions != rhs.reactions {
return false
}
if lhs.interactionsGraph != rhs.interactionsGraph {
return false
}
if lhs.interactionsGraphDelta != rhs.interactionsGraphDelta {
return false
}
if lhs.reactionsGraph != rhs.reactionsGraph {
return false
}
return true
}
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> MessageStats {
return MessageStats(views: self.views, forwards: self.forwards, reactions: self.reactions, interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta, reactionsGraph: self.reactionsGraph)
}
}
public struct MessageStatsContextState: Equatable {
public var stats: MessageStats?
}
private func requestMessageStats(postbox: Postbox, network: Network, messageId: MessageId, dark: Bool = false) -> Signal<MessageStats?, NoError> {
return postbox.transaction { transaction -> (Int32, Peer, Message)? in
if let peer = transaction.getPeer(messageId.peerId), let message = transaction.getMessage(messageId), let cachedData = transaction.getPeerCachedData(peerId: messageId.peerId) as? CachedChannelData {
return (cachedData.statsDatacenterId, peer, message)
} else {
return nil
}
} |> mapToSignal { data -> Signal<MessageStats?, NoError> in
guard let (datacenterId, peer, message) = data, let inputChannel = apiInputChannel(peer) else {
return .never()
}
var flags: Int32 = 0
if dark {
flags |= (1 << 1)
}
let request = Api.functions.stats.getMessageStats(flags: flags, channel: inputChannel, msgId: messageId.id)
let signal: Signal<Api.stats.MessageStats, MTRpcError>
if network.datacenterId != datacenterId {
signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|> castError(MTRpcError.self)
|> mapToSignal { worker in
return worker.request(request)
}
} else {
signal = network.request(request)
}
var views: Int = 0
var forwards: Int = 0
var reactions: Int = 0
for attribute in message.attributes {
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
views = viewsAttribute.count
} else if let forwardsAttribute = attribute as? ForwardCountMessageAttribute {
forwards = forwardsAttribute.count
} else if let reactionsAttribute = attribute as? ReactionsMessageAttribute {
reactions = Int(reactionsAttribute.reactions.reduce(0, { partialResult, reaction in
return partialResult + reaction.count
}))
}
}
return signal
|> mapToSignal { result -> Signal<MessageStats?, MTRpcError> in
if case let .messageStats(apiInteractionsGraph, apiReactionsGraph) = result {
let interactionsGraph = StatsGraph(apiStatsGraph: apiInteractionsGraph)
var interactionsGraphDelta: Int64 = 86400
if case let .Loaded(_, data) = interactionsGraph {
if let start = data.range(of: "[\"x\",") {
let substring = data.suffix(from: start.upperBound)
if let end = substring.range(of: "],") {
let valuesString = substring.prefix(through: substring.index(before: end.lowerBound))
let values = valuesString.components(separatedBy: ",").compactMap { Int64($0) }
if values.count > 1 {
let first = values[0]
let second = values[1]
let delta = abs(second - first) / 1000
interactionsGraphDelta = delta
}
}
}
}
let reactionsGraph = StatsGraph(apiStatsGraph: apiReactionsGraph)
return .single(MessageStats(
views: views,
forwards: forwards,
reactions: reactions,
interactionsGraph: interactionsGraph,
interactionsGraphDelta: interactionsGraphDelta,
reactionsGraph: reactionsGraph
))
} else {
return .single(nil)
}
}
|> retryRequest
}
}
private final class MessageStatsContextImpl {
private let postbox: Postbox
private let network: Network
private let messageId: MessageId
private var _state: MessageStatsContextState {
didSet {
if self._state != oldValue {
self._statePromise.set(.single(self._state))
}
}
}
private let _statePromise = Promise<MessageStatsContextState>()
var state: Signal<MessageStatsContextState, NoError> {
return self._statePromise.get()
}
private let disposable = MetaDisposable()
private let disposables = DisposableDict<String>()
init(postbox: Postbox, network: Network, messageId: MessageId) {
assert(Queue.mainQueue().isCurrent())
self.postbox = postbox
self.network = network
self.messageId = messageId
self._state = MessageStatsContextState(stats: nil)
self._statePromise.set(.single(self._state))
self.load()
}
deinit {
assert(Queue.mainQueue().isCurrent())
self.disposable.dispose()
self.disposables.dispose()
}
private func load() {
assert(Queue.mainQueue().isCurrent())
self.disposable.set((requestMessageStats(postbox: self.postbox, network: self.network, messageId: self.messageId)
|> deliverOnMainQueue).start(next: { [weak self] stats in
if let strongSelf = self {
strongSelf._state = MessageStatsContextState(stats: stats)
strongSelf._statePromise.set(.single(strongSelf._state))
}
}))
}
func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
if let token = graph.token {
return requestGraph(postbox: self.postbox, network: self.network, peerId: self.messageId.peerId, token: token, x: x)
} else {
return .single(nil)
}
}
}
public final class MessageStatsContext {
private let impl: QueueLocalObject<MessageStatsContextImpl>
public var state: Signal<MessageStatsContextState, NoError> {
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, messageId: MessageId) {
self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: {
return MessageStatsContextImpl(postbox: account.postbox, network: account.network, messageId: messageId)
})
}
public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
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
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,481 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
import MtProtoKit
public struct StarsRevenueStats: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case topHoursGraph
case revenueGraph
case balances
case usdRate
}
static func key(peerId: PeerId, ton: Bool) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 4)
key.setInt64(0, value: peerId.toInt64())
key.setInt32(8, value: ton ? 1 : 0)
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: CurrencyAmount
public let availableBalance: CurrencyAmount
public let overallRevenue: CurrencyAmount
public let withdrawEnabled: Bool
public let nextWithdrawalTimestamp: Int32?
public init(
currentBalance: CurrencyAmount,
availableBalance: CurrencyAmount,
overallRevenue: CurrencyAmount,
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(StarsAmount.self, forKey: .currentBalanceStars) {
self.currentBalance = CurrencyAmount(amount: legacyCurrentBalance, currency: .stars)
} else {
self.currentBalance = try container.decode(CurrencyAmount.self, forKey: .currentBalance)
}
if let legacyAvailableBalance = try container.decodeIfPresent(StarsAmount.self, forKey: .availableBalanceStars) {
self.availableBalance = CurrencyAmount(amount: legacyAvailableBalance, currency: .stars)
} else {
self.availableBalance = try container.decode(CurrencyAmount.self, forKey: .availableBalance)
}
if let legacyOverallRevenue = try container.decodeIfPresent(StarsAmount.self, forKey: .overallRevenueStars) {
self.overallRevenue = CurrencyAmount(amount: legacyOverallRevenue, currency: .stars)
} else {
self.overallRevenue = try container.decode(CurrencyAmount.self, forKey: .overallRevenue)
}
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: .currentBalance)
try container.encode(self.availableBalance, forKey: .availableBalance)
try container.encode(self.overallRevenue, forKey: .overallRevenue)
try container.encode(self.withdrawEnabled, forKey: .withdrawEnabled)
try container.encodeIfPresent(self.nextWithdrawalTimestamp, forKey: .nextWithdrawalTimestamp)
}
}
public let topHoursGraph: StatsGraph?
public let revenueGraph: StatsGraph
public let balances: Balances
public let usdRate: Double
init(topHoursGraph: StatsGraph?, revenueGraph: StatsGraph, balances: Balances, usdRate: Double) {
self.topHoursGraph = topHoursGraph
self.revenueGraph = revenueGraph
self.balances = balances
self.usdRate = usdRate
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.topHoursGraph = try container.decodeIfPresent(StatsGraph.self, forKey: .topHoursGraph)
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.encodeIfPresent(self.topHoursGraph, forKey: .topHoursGraph)
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.topHoursGraph != rhs.topHoursGraph {
return false
}
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(
topHoursGraph: self.topHoursGraph,
revenueGraph: self.revenueGraph,
balances: balances,
usdRate: self.usdRate
)
}
}
extension StarsRevenueStats {
init(apiStarsRevenueStats: Api.payments.StarsRevenueStats, peerId: PeerId) {
switch apiStarsRevenueStats {
case let .starsRevenueStats(_, topHoursGraph, revenueGraph, balances, usdRate):
self.init(topHoursGraph: topHoursGraph.flatMap { StatsGraph(apiStatsGraph: $0) }, 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: CurrencyAmount(apiAmount: currentBalance), availableBalance: CurrencyAmount(apiAmount: availableBalance), overallRevenue: CurrencyAmount(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, ton: Bool, dark: Bool = false) -> Signal<StarsRevenueStats?, NoError> {
return postbox.transaction { transaction -> Peer? in
if let peer = transaction.getPeer(peerId) {
return peer
}
return nil
} |> mapToSignal { peer -> Signal<StarsRevenueStats?, NoError> in
guard let peer, let inputPeer = apiInputPeer(peer) else {
return .never()
}
var flags: Int32 = 0
if ton {
flags |= (1 << 1)
}
if dark {
flags |= (1 << 0)
}
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 let ton: Bool
private var _state: StarsRevenueStatsContextState {
didSet {
if self._state != oldValue {
self._statePromise.set(.single(self._state))
}
}
}
private let _statePromise = Promise<StarsRevenueStatsContextState>()
var state: Signal<StarsRevenueStatsContextState, NoError> {
return self._statePromise.get()
}
private let disposable = MetaDisposable()
private let updateDisposable = MetaDisposable()
init(account: Account, peerId: PeerId, ton: Bool) {
assert(Queue.mainQueue().isCurrent())
self.account = account
self.peerId = peerId
self.ton = ton
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, ton: ton)))?.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 ton = self.ton
let signal = requestStarsRevenueStats(postbox: self.account.postbox, network: self.account.network, peerId: self.peerId, ton: self.ton)
|> mapToSignal { initial -> Signal<StarsRevenueStats?, NoError> 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, ton: ton)), entry: entry)
}
}).start()
}
}
}))
}
func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
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<StarsRevenueStatsContextImpl>
public var state: Signal<StarsRevenueStatsContextState, NoError> {
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, ton: Bool) {
self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: {
return StarsRevenueStatsContextImpl(account: account, peerId: peerId, ton: ton)
})
}
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<StatsGraph?, NoError> {
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<Never, RequestStarsRevenueWithdrawalError> {
return account.network.request(Api.functions.payments.getStarsRevenueWithdrawalUrl(flags: 0, peer: .inputPeerEmpty, amount: nil, 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, ton: Bool, peerId: PeerId, amount: Int64?, password: String) -> Signal<String, RequestStarsRevenueWithdrawalError> {
guard !password.isEmpty else {
return .fail(.invalidPassword)
}
return account.postbox.transaction { transaction -> Signal<String, RequestStarsRevenueWithdrawalError> 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<Api.InputCheckPasswordSRP, RequestStarsRevenueWithdrawalError> 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<String, RequestStarsRevenueWithdrawalError> in
var flags: Int32 = 0
if ton {
flags |= 1 << 0
} else {
flags |= 1 << 1
}
return account.network.request(Api.functions.payments.getStarsRevenueWithdrawalUrl(flags: flags, peer: inputPeer, amount: 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<String?, NoError> {
return account.postbox.transaction { transaction -> Signal<String?, NoError> 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<Api.payments.StarsRevenueAdsAccountUrl?, NoError> in
return .single(nil)
}
|> map { result -> String? in
guard let result else {
return nil
}
switch result {
case let .starsRevenueAdsAccountUrl(url):
return url
}
}
}
|> switchToLatest
}
@@ -0,0 +1,450 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
import MtProtoKit
public struct StoryStats: Equatable {
public let interactionsGraph: StatsGraph
public let interactionsGraphDelta: Int64
public let reactionsGraph: StatsGraph
init(interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) {
self.interactionsGraph = interactionsGraph
self.interactionsGraphDelta = interactionsGraphDelta
self.reactionsGraph = reactionsGraph
}
public static func == (lhs: StoryStats, rhs: StoryStats) -> Bool {
if lhs.interactionsGraph != rhs.interactionsGraph {
return false
}
if lhs.interactionsGraphDelta != rhs.interactionsGraphDelta {
return false
}
if lhs.reactionsGraph != rhs.reactionsGraph {
return false
}
return true
}
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> StoryStats {
return StoryStats(interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta, reactionsGraph: self.reactionsGraph)
}
}
public struct StoryStatsContextState: Equatable {
public var stats: StoryStats?
}
private func requestStoryStats(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: EnginePeer.Id, storyId: Int32, dark: Bool = false) -> Signal<StoryStats?, NoError> {
return postbox.transaction { transaction -> (Int32, Peer)? in
if let peer = transaction.getPeer(peerId), let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
return (cachedData.statsDatacenterId, peer)
} else {
return nil
}
}
|> mapToSignal { data -> Signal<StoryStats?, NoError> in
guard let (statsDatacenterId, peer) = data, let inputPeer = apiInputPeer(peer) else {
return .never()
}
var flags: Int32 = 0
if dark {
flags |= (1 << 1)
}
let request = Api.functions.stats.getStoryStats(flags: flags, peer: inputPeer, id: storyId)
let signal: Signal<Api.stats.StoryStats, MTRpcError>
if network.datacenterId != statsDatacenterId {
signal = network.download(datacenterId: Int(statsDatacenterId), isMedia: false, tag: nil)
|> castError(MTRpcError.self)
|> mapToSignal { worker in
return worker.request(request)
}
} else {
signal = network.request(request)
}
return signal
|> mapToSignal { result -> Signal<StoryStats?, MTRpcError> in
if case let .storyStats(apiInteractionsGraph, apiReactionsGraph) = result {
let interactionsGraph = StatsGraph(apiStatsGraph: apiInteractionsGraph)
var interactionsGraphDelta: Int64 = 86400
if case let .Loaded(_, data) = interactionsGraph {
if let start = data.range(of: "[\"x\",") {
let substring = data.suffix(from: start.upperBound)
if let end = substring.range(of: "],") {
let valuesString = substring.prefix(through: substring.index(before: end.lowerBound))
let values = valuesString.components(separatedBy: ",").compactMap { Int64($0) }
if values.count > 1 {
let first = values[0]
let second = values[1]
let delta = abs(second - first) / 1000
interactionsGraphDelta = delta
}
}
}
}
let reactionsGraph = StatsGraph(apiStatsGraph: apiReactionsGraph)
return .single(StoryStats(
interactionsGraph: interactionsGraph,
interactionsGraphDelta: interactionsGraphDelta,
reactionsGraph: reactionsGraph
))
} else {
return .single(nil)
}
}
|> retryRequest
}
}
private final class StoryStatsContextImpl {
private let accountPeerId: EnginePeer.Id
private let postbox: Postbox
private let network: Network
private let peerId: EnginePeer.Id
private let storyId: Int32
private var _state: StoryStatsContextState {
didSet {
if self._state != oldValue {
self._statePromise.set(.single(self._state))
}
}
}
private let _statePromise = Promise<StoryStatsContextState>()
var state: Signal<StoryStatsContextState, NoError> {
return self._statePromise.get()
}
private let disposable = MetaDisposable()
private let disposables = DisposableDict<String>()
init(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, peerId: EnginePeer.Id, storyId: Int32) {
assert(Queue.mainQueue().isCurrent())
self.accountPeerId = accountPeerId
self.postbox = postbox
self.network = network
self.peerId = peerId
self.storyId = storyId
self._state = StoryStatsContextState(stats: nil)
self._statePromise.set(.single(self._state))
self.load()
}
deinit {
assert(Queue.mainQueue().isCurrent())
self.disposable.dispose()
self.disposables.dispose()
}
private func load() {
assert(Queue.mainQueue().isCurrent())
self.disposable.set((requestStoryStats(accountPeerId: self.accountPeerId, postbox: self.postbox, network: self.network, peerId: self.peerId, storyId: self.storyId)
|> deliverOnMainQueue).start(next: { [weak self] stats in
if let strongSelf = self {
strongSelf._state = StoryStatsContextState(stats: stats)
strongSelf._statePromise.set(.single(strongSelf._state))
}
}))
}
func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
if let token = graph.token {
return requestGraph(postbox: self.postbox, network: self.network, peerId: self.peerId, token: token, x: x)
} else {
return .single(nil)
}
}
}
public final class StoryStatsContext {
private let impl: QueueLocalObject<StoryStatsContextImpl>
public var state: Signal<StoryStatsContextState, NoError> {
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: EnginePeer.Id, storyId: Int32) {
self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: {
return StoryStatsContextImpl(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, peerId: peerId, storyId: storyId)
})
}
public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
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
}
}
}
private final class StoryStatsPublicForwardsContextImpl {
private let queue: Queue
private let account: Account
private let subject: StoryStatsPublicForwardsContext.Subject
private let disposable = MetaDisposable()
private var isLoadingMore: Bool = false
private var hasLoadedOnce: Bool = false
private var canLoadMore: Bool = true
private var results: [StoryStatsPublicForwardsContext.State.Forward] = []
private var count: Int32
private var lastOffset: String?
let state = Promise<StoryStatsPublicForwardsContext.State>()
init(queue: Queue, account: Account, subject: StoryStatsPublicForwardsContext.Subject) {
self.queue = queue
self.account = account
self.subject = subject
self.count = 0
self.loadMore()
}
deinit {
self.disposable.dispose()
}
func reload() {
self.loadMore()
}
func loadMore() {
if self.isLoadingMore || !self.canLoadMore {
return
}
self.isLoadingMore = true
let account = self.account
let accountPeerId = account.peerId
let subject = self.subject
let lastOffset = self.lastOffset
self.disposable.set((self.account.postbox.transaction { transaction -> (Peer, Int32?)? in
let peerId: PeerId
switch subject {
case let .story(peerIdValue, _):
peerId = peerIdValue
case let .message(messageId):
peerId = messageId.peerId
}
let statsDatacenterId = (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.statsDatacenterId
guard let peer = transaction.getPeer(peerId) else {
return nil
}
return (peer, statsDatacenterId)
}
|> mapToSignal { data -> Signal<([StoryStatsPublicForwardsContext.State.Forward], Int32, String?), NoError> in
if let (peer, statsDatacenterId) = data {
let offset = lastOffset ?? ""
let request: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stats.PublicForwards>)
switch subject {
case let .story(_, id):
guard let inputPeer = apiInputPeer(peer) else {
return .complete()
}
request = Api.functions.stats.getStoryPublicForwards(peer: inputPeer, id: id, offset: offset, limit: 50)
case let .message(messageId):
guard let inputChannel = apiInputChannel(peer) else {
return .complete()
}
request = Api.functions.stats.getMessagePublicForwards(channel: inputChannel, msgId: messageId.id, offset: offset, limit: 50)
}
let signal: Signal<Api.stats.PublicForwards, MTRpcError>
if let statsDatacenterId = statsDatacenterId, account.network.datacenterId != statsDatacenterId {
signal = account.network.download(datacenterId: Int(statsDatacenterId), isMedia: false, tag: nil)
|> castError(MTRpcError.self)
|> mapToSignal { worker in
return worker.request(request)
}
} else {
signal = account.network.request(request, automaticFloodWait: false)
}
return signal
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.stats.PublicForwards?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<([StoryStatsPublicForwardsContext.State.Forward], Int32, String?), NoError> in
return account.postbox.transaction { transaction -> ([StoryStatsPublicForwardsContext.State.Forward], Int32, String?) in
guard let result = result else {
return ([], 0, nil)
}
switch result {
case let .publicForwards(_, count, forwards, nextOffset, chats, users):
var peers: [PeerId: Peer] = [:]
for user in users {
if let user = TelegramUser.merge(transaction.getPeer(user.peerId) as? TelegramUser, rhs: user) {
peers[user.id] = user
}
}
for chat in chats {
if let groupOrChannel = mergeGroupOrChannel(lhs: transaction.getPeer(chat.peerId), rhs: chat) {
peers[groupOrChannel.id] = groupOrChannel
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(peers: Array(peers.values)))
var resultForwards: [StoryStatsPublicForwardsContext.State.Forward] = []
for forward in forwards {
switch forward {
case let .publicForwardMessage(apiMessage):
if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: false), let renderedMessage = locallyRenderedMessage(message: message, peers: peers) {
resultForwards.append(.message(EngineMessage(renderedMessage)))
}
case let .publicForwardStory(apiPeer, apiStory):
if let storedItem = Stories.StoredItem(apiStoryItem: apiStory, peerId: apiPeer.peerId, transaction: transaction), case let .item(item) = storedItem, let media = item.media, let peer = peers[apiPeer.peerId] {
let mappedItem = EngineStoryItem(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: EngineMedia(media),
alternativeMediaList: item.alternativeMediaList.map(EngineMedia.init),
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: item.views.flatMap { views in
return EngineStoryItem.Views(
seenCount: views.seenCount,
reactedCount: views.reactedCount,
forwardCount: views.forwardCount,
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
return transaction.getPeer(id).flatMap(EnginePeer.init)
},
reactions: views.reactions,
hasList: views.hasList
)
},
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isPending: false,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction,
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) },
author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) },
folderIds: item.folderIds
)
resultForwards.append(.story(EnginePeer(peer), mappedItem))
}
}
}
return (resultForwards, count, nextOffset)
}
}
}
} else {
return .single(([], 0, nil))
}
}
|> deliverOn(self.queue)).start(next: { [weak self] forwards, updatedCount, nextOffset in
guard let strongSelf = self else {
return
}
strongSelf.lastOffset = nextOffset
for forward in forwards {
strongSelf.results.append(forward)
}
strongSelf.isLoadingMore = false
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = !forwards.isEmpty && nextOffset != nil
if strongSelf.canLoadMore {
strongSelf.count = max(updatedCount, Int32(strongSelf.results.count))
} else {
strongSelf.count = Int32(strongSelf.results.count)
}
strongSelf.updateState()
}))
self.updateState()
}
private func updateState() {
self.state.set(.single(StoryStatsPublicForwardsContext.State(forwards: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count)))
}
}
public final class StoryStatsPublicForwardsContext {
public struct State: Equatable {
public enum Forward: Equatable {
case message(EngineMessage)
case story(EnginePeer, EngineStoryItem)
}
public var forwards: [Forward]
public var isLoadingMore: Bool
public var hasLoadedOnce: Bool
public var canLoadMore: Bool
public var count: Int32
public static var Empty = State(forwards: [], isLoadingMore: false, hasLoadedOnce: true, canLoadMore: false, count: 0)
public static var Loading = State(forwards: [], isLoadingMore: false, hasLoadedOnce: false, canLoadMore: false, count: 0)
}
private let queue: Queue = Queue()
private let impl: QueueLocalObject<StoryStatsPublicForwardsContextImpl>
public var state: Signal<State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.state.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public enum Subject {
case story(peerId: EnginePeer.Id, id: Int32)
case message(messageId: EngineMessage.Id)
}
public init(account: Account, subject: Subject) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return StoryStatsPublicForwardsContextImpl(queue: queue, account: account, subject: subject)
})
}
public func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
public func reload() {
self.impl.with { impl in
impl.reload()
}
}
}