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,227 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public struct ChangeAccountPhoneNumberData: Equatable {
public let type: SentAuthorizationCodeType
public let hash: String
public let timeout: Int32?
public let nextType: AuthorizationCodeNextType?
public static func ==(lhs: ChangeAccountPhoneNumberData, rhs: ChangeAccountPhoneNumberData) -> Bool {
if lhs.type != rhs.type {
return false
}
if lhs.hash != rhs.hash {
return false
}
if lhs.timeout != rhs.timeout {
return false
}
if lhs.nextType != rhs.nextType {
return false
}
return true
}
}
public enum RequestChangeAccountPhoneNumberVerificationError {
case invalidPhoneNumber
case limitExceeded
case phoneNumberOccupied
case phoneBanned
case generic
}
func _internal_requestChangeAccountPhoneNumberVerification(account: Account, apiId: Int32, apiHash: String, phoneNumber: String, pushNotificationConfiguration: AuthorizationCodePushNotificationConfiguration?, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> {
var flags: Int32 = 0
flags |= 1 << 5 //allowMissedCall
var token: String?
var appSandbox: Api.Bool?
if let pushNotificationConfiguration = pushNotificationConfiguration {
flags |= 1 << 7
flags |= 1 << 8
token = pushNotificationConfiguration.token
appSandbox = pushNotificationConfiguration.isSandbox ? .boolTrue : .boolFalse
}
return account.network.request(Api.functions.account.sendChangePhoneCode(phoneNumber: phoneNumber, settings: .codeSettings(flags: flags, logoutTokens: nil, token: token, appSandbox: appSandbox)), automaticFloodWait: false)
|> mapError { error -> RequestChangeAccountPhoneNumberVerificationError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "PHONE_NUMBER_INVALID" {
return .invalidPhoneNumber
} else if error.errorDescription == "PHONE_NUMBER_OCCUPIED" {
return .phoneNumberOccupied
} else if error.errorDescription == "PHONE_NUMBER_BANNED" {
return .phoneBanned
} else {
return .generic
}
}
|> mapToSignal { sentCode -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> in
switch sentCode {
case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout):
var parsedNextType: AuthorizationCodeNextType?
if let nextType = nextType {
parsedNextType = AuthorizationCodeNextType(apiType: nextType)
}
if case let .sentCodeTypeFirebaseSms(_, _, _, _, receipt, pushTimeout, _) = type {
return firebaseSecretStream
|> map { mapping -> String? in
guard let receipt = receipt else {
return nil
}
if let value = mapping[receipt] {
return value
}
if receipt == "" && mapping.count == 1 {
return mapping.first?.value
}
return nil
}
|> filter { $0 != nil }
|> take(1)
|> timeout(Double(pushTimeout ?? 15), queue: .mainQueue(), alternate: .single(nil))
|> castError(RequestChangeAccountPhoneNumberVerificationError.self)
|> mapToSignal { firebaseSecret -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> in
guard let firebaseSecret = firebaseSecret else {
return internalResendChangeAccountPhoneNumberVerification(account: account, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, apiId: apiId, apiHash: apiHash, firebaseSecretStream: firebaseSecretStream, reason: .firebasePushTimeout)
}
return sendFirebaseAuthorizationCode(network: account.network, phoneNumber: phoneNumber, apiId: apiId, apiHash: apiHash, phoneCodeHash: phoneCodeHash, timeout: codeTimeout, firebaseSecret: firebaseSecret)
|> `catch` { _ -> Signal<Bool, SendFirebaseAuthorizationCodeError> in
return .single(false)
}
|> mapError { _ -> RequestChangeAccountPhoneNumberVerificationError in
return .generic
}
|> mapToSignal { success -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> in
if success {
return .single(ChangeAccountPhoneNumberData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType))
} else {
return internalResendChangeAccountPhoneNumberVerification(account: account, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, apiId: apiId, apiHash: apiHash, firebaseSecretStream: firebaseSecretStream, reason: .firebaseSendCodeError)
}
}
}
} else {
return .single(ChangeAccountPhoneNumberData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType))
}
case .sentCodeSuccess, .sentCodePaymentRequired:
return .never()
}
}
}
private func internalResendChangeAccountPhoneNumberVerification(account: Account, phoneNumber: String, phoneCodeHash: String, apiId: Int32, apiHash: String, firebaseSecretStream: Signal<[String: String], NoError>, reason: ResendAuthorizationCodeReason?) -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> {
var flags: Int32 = 0
var mappedReason: String?
if let reason {
flags |= 1 << 0
mappedReason = reason.rawValue
}
return account.network.request(Api.functions.auth.resendCode(flags: flags, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, reason: mappedReason), automaticFloodWait: false)
|> mapError { error -> RequestChangeAccountPhoneNumberVerificationError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "PHONE_NUMBER_INVALID" {
return .invalidPhoneNumber
} else if error.errorDescription == "PHONE_NUMBER_OCCUPIED" {
return .phoneNumberOccupied
} else {
return .generic
}
}
|> mapToSignal { sentCode -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> in
switch sentCode {
case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout):
var parsedNextType: AuthorizationCodeNextType?
if let nextType = nextType {
parsedNextType = AuthorizationCodeNextType(apiType: nextType)
}
if case let .sentCodeTypeFirebaseSms(_, _, _, _, receipt, pushTimeout, _) = type {
return firebaseSecretStream
|> map { mapping -> String? in
guard let receipt = receipt else {
return nil
}
if let value = mapping[receipt] {
return value
}
if receipt == "" && mapping.count == 1 {
return mapping.first?.value
}
return nil
}
|> filter { $0 != nil }
|> take(1)
|> timeout(Double(pushTimeout ?? 15), queue: .mainQueue(), alternate: .single(nil))
|> castError(RequestChangeAccountPhoneNumberVerificationError.self)
|> mapToSignal { firebaseSecret -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> in
guard let firebaseSecret = firebaseSecret else {
return internalResendChangeAccountPhoneNumberVerification(account: account, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, apiId: apiId, apiHash: apiHash, firebaseSecretStream: firebaseSecretStream, reason: .firebasePushTimeout)
}
return sendFirebaseAuthorizationCode(network: account.network, phoneNumber: phoneNumber, apiId: apiId, apiHash: apiHash, phoneCodeHash: phoneCodeHash, timeout: codeTimeout, firebaseSecret: firebaseSecret)
|> `catch` { _ -> Signal<Bool, SendFirebaseAuthorizationCodeError> in
return .single(false)
}
|> mapError { _ -> RequestChangeAccountPhoneNumberVerificationError in
return .generic
}
|> mapToSignal { success -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> in
if success {
return .single(ChangeAccountPhoneNumberData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType))
} else {
return internalResendChangeAccountPhoneNumberVerification(account: account, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, apiId: apiId, apiHash: apiHash, firebaseSecretStream: firebaseSecretStream, reason: .firebaseSendCodeError)
}
}
}
} else {
return .single(ChangeAccountPhoneNumberData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType))
}
case .sentCodeSuccess, .sentCodePaymentRequired:
return .never()
}
}
}
func _internal_requestNextChangeAccountPhoneNumberVerification(account: Account, phoneNumber: String, phoneCodeHash: String, apiId: Int32, apiHash: String, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> {
return internalResendChangeAccountPhoneNumberVerification(account: account, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, apiId: apiId, apiHash: apiHash, firebaseSecretStream: firebaseSecretStream, reason: nil)
}
public enum ChangeAccountPhoneNumberError {
case generic
case invalidCode
case codeExpired
case limitExceeded
}
func _internal_requestChangeAccountPhoneNumber(account: Account, phoneNumber: String, phoneCodeHash: String, phoneCode: String) -> Signal<Void, ChangeAccountPhoneNumberError> {
let accountPeerId = account.peerId
return account.network.request(Api.functions.account.changePhone(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, phoneCode: phoneCode), automaticFloodWait: false)
|> mapError { error -> ChangeAccountPhoneNumberError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "PHONE_CODE_INVALID" {
return .invalidCode
} else if error.errorDescription == "PHONE_CODE_EXPIRED" {
return .codeExpired
} else {
return .generic
}
}
|> mapToSignal { result -> Signal<Void, ChangeAccountPhoneNumberError> in
return account.postbox.transaction { transaction -> Void in
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(transaction: transaction, chats: [], users: [result]))
} |> mapError { _ -> ChangeAccountPhoneNumberError in }
}
}
@@ -0,0 +1,56 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
public enum NotificationTokenType {
case aps(encrypt: Bool)
case voip
}
func _internal_unregisterNotificationToken(account: Account, token: Data, type: NotificationTokenType, otherAccountUserIds: [PeerId.Id]) -> Signal<Never, NoError> {
let mappedType: Int32
switch type {
case .aps:
mappedType = 1
case .voip:
mappedType = 9
}
return account.network.request(Api.functions.account.unregisterDevice(tokenType: mappedType, token: hexString(token), otherUids: otherAccountUserIds.map({ $0._internalGetInt64Value() })))
|> retryRequest
|> ignoreValues
}
func _internal_registerNotificationToken(account: Account, token: Data, type: NotificationTokenType, sandbox: Bool, otherAccountUserIds: [PeerId.Id], excludeMutedChats: Bool) -> Signal<Bool, NoError> {
return masterNotificationsKey(account: account, ignoreDisabled: false)
|> mapToSignal { masterKey -> Signal<Bool, NoError> in
let mappedType: Int32
var keyData = Data()
switch type {
case let .aps(encrypt):
mappedType = 1
if encrypt {
keyData = masterKey.data
}
case .voip:
mappedType = 9
keyData = masterKey.data
}
var flags: Int32 = 0
if excludeMutedChats {
flags |= 1 << 0
}
return account.network.request(Api.functions.account.registerDevice(flags: flags, tokenType: mappedType, token: hexString(token), appSandbox: sandbox ? .boolTrue : .boolFalse, secret: Buffer(data: keyData), otherUids: otherAccountUserIds.map({ $0._internalGetInt64Value() })))
|> map { _ -> Bool in
return true
}
|> `catch` { error -> Signal<Bool, NoError> in
if error.errorDescription == "TOKEN_WAS_INVALIDATED" {
return .single(false)
} else {
return .single(true)
}
}
}
}
@@ -0,0 +1,301 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
public extension TelegramEngine {
final class AccountData {
private let account: Account
init(account: Account) {
self.account = account
}
public func acceptTermsOfService(id: String) -> Signal<Void, NoError> {
return _internal_acceptTermsOfService(account: self.account, id: id)
}
public func requestChangeAccountPhoneNumberVerification(apiId: Int32, apiHash: String, phoneNumber: String, pushNotificationConfiguration: AuthorizationCodePushNotificationConfiguration?, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> {
return _internal_requestChangeAccountPhoneNumberVerification(account: self.account, apiId: apiId, apiHash: apiHash, phoneNumber: phoneNumber, pushNotificationConfiguration: pushNotificationConfiguration, firebaseSecretStream: firebaseSecretStream)
}
public func requestNextChangeAccountPhoneNumberVerification(phoneNumber: String, phoneCodeHash: String, apiId: Int32, apiHash: String, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal<ChangeAccountPhoneNumberData, RequestChangeAccountPhoneNumberVerificationError> {
return _internal_requestNextChangeAccountPhoneNumberVerification(account: self.account, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, apiId: apiId, apiHash: apiHash, firebaseSecretStream: firebaseSecretStream)
}
public func requestChangeAccountPhoneNumber(phoneNumber: String, phoneCodeHash: String, phoneCode: String) -> Signal<Void, ChangeAccountPhoneNumberError> {
return _internal_requestChangeAccountPhoneNumber(account: self.account, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, phoneCode: phoneCode)
}
public func updateAccountPeerName(firstName: String, lastName: String) -> Signal<Void, NoError> {
return _internal_updateAccountPeerName(account: self.account, firstName: firstName, lastName: lastName)
}
public func updateAbout(about: String?) -> Signal<Void, UpdateAboutError> {
return _internal_updateAbout(account: self.account, about: about)
}
public func updateBirthday(birthday: TelegramBirthday?) -> Signal<Never, UpdateBirthdayError> {
return _internal_updateBirthday(account: self.account, birthday: birthday)
}
public func observeAvailableColorOptions(scope: PeerColorsScope) -> Signal<EngineAvailableColorOptions, NoError> {
return _internal_observeAvailableColorOptions(postbox: self.account.postbox, scope: scope)
}
public func updateNameColorAndEmoji(nameColor: UpdateNameColor, profileColor: PeerNameColor?, profileBackgroundEmojiId: Int64?) -> Signal<Void, UpdateNameColorAndEmojiError> {
return _internal_updateNameColorAndEmoji(account: self.account, nameColor: nameColor, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId)
}
public func unregisterNotificationToken(token: Data, type: NotificationTokenType, otherAccountUserIds: [PeerId.Id]) -> Signal<Never, NoError> {
return _internal_unregisterNotificationToken(account: self.account, token: token, type: type, otherAccountUserIds: otherAccountUserIds)
}
public func registerNotificationToken(token: Data, type: NotificationTokenType, sandbox: Bool, otherAccountUserIds: [PeerId.Id], excludeMutedChats: Bool) -> Signal<Bool, NoError> {
return _internal_registerNotificationToken(account: self.account, token: token, type: type, sandbox: sandbox, otherAccountUserIds: otherAccountUserIds, excludeMutedChats: excludeMutedChats)
}
public func updateAccountPhoto(resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> {
return _internal_updateAccountPhoto(account: self.account, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, fallback: false, mapResourceToAvatarSizes: mapResourceToAvatarSizes)
}
public func updatePeerPhotoExisting(reference: TelegramMediaImageReference) -> Signal<TelegramMediaImage?, NoError> {
return _internal_updatePeerPhotoExisting(network: self.account.network, reference: reference)
}
public func removeAccountPhoto(reference: TelegramMediaImageReference?) -> Signal<Void, NoError> {
return _internal_removeAccountPhoto(account: self.account, reference: reference, fallback: false)
}
public func updateFallbackPhoto(resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> {
return _internal_updateAccountPhoto(account: self.account, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, fallback: true, mapResourceToAvatarSizes: mapResourceToAvatarSizes)
}
public func removeFallbackPhoto(reference: TelegramMediaImageReference?) -> Signal<Void, NoError> {
return _internal_removeAccountPhoto(account: self.account, reference: reference, fallback: true)
}
public func setStarGiftStatus(starGift: StarGift.UniqueGift, expirationDate: Int32?) -> Signal<Never, NoError> {
let peerId = self.account.peerId
var flags: Int32 = 0
if let _ = expirationDate {
flags |= (1 << 0)
}
var file: TelegramMediaFile?
var patternFile: TelegramMediaFile?
var innerColor: Int32?
var outerColor: Int32?
var patternColor: Int32?
var textColor: Int32?
for attribute in starGift.attributes {
switch attribute {
case let .model(_, fileValue, _):
file = fileValue
case let .pattern(_, patternFileValue, _):
patternFile = patternFileValue
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
innerColor = innerColorValue
outerColor = outerColorValue
patternColor = patternColorValue
textColor = textColorValue
default:
break
}
}
let apiEmojiStatus: Api.EmojiStatus
var emojiStatus: PeerEmojiStatus?
if let file, let patternFile, let innerColor, let outerColor, let patternColor, let textColor {
apiEmojiStatus = .inputEmojiStatusCollectible(flags: flags, collectibleId: starGift.id, until: expirationDate)
emojiStatus = PeerEmojiStatus(content: .starGift(id: starGift.id, fileId: file.fileId.id, title: starGift.title, slug: starGift.slug, patternFileId: patternFile.fileId.id, innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor), expirationDate: expirationDate)
} else {
apiEmojiStatus = .emojiStatusEmpty
}
let remoteApply = self.account.network.request(Api.functions.account.updateEmojiStatus(emojiStatus: apiEmojiStatus))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
return self.account.postbox.transaction { transaction -> Void in
if let file, let patternFile {
transaction.storeMediaIfNotPresent(media: file)
transaction.storeMediaIfNotPresent(media: patternFile)
}
if let peer = transaction.getPeer(peerId) as? TelegramUser {
updatePeersCustom(transaction: transaction, peers: [
peer.withUpdatedEmojiStatus(emojiStatus)
], update: { _, updated in
updated
})
}
}
|> ignoreValues
|> then(remoteApply)
}
public func setEmojiStatus(file: TelegramMediaFile?, expirationDate: Int32?) -> Signal<Never, NoError> {
let peerId = self.account.peerId
let remoteApply = self.account.network.request(Api.functions.account.updateEmojiStatus(emojiStatus: file.flatMap({ file in
var flags: Int32 = 0
if let _ = expirationDate {
flags |= (1 << 0)
}
return Api.EmojiStatus.emojiStatus(flags: flags, documentId: file.fileId.id, until: expirationDate)
}) ?? Api.EmojiStatus.emojiStatusEmpty))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
return self.account.postbox.transaction { transaction -> Void in
if let file = file {
transaction.storeMediaIfNotPresent(media: file)
if let entry = CodableEntry(RecentMediaItem(file)) {
let itemEntry = OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry)
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStatusEmoji, item: itemEntry, removeTailIfCountExceeds: 32)
}
}
if let peer = transaction.getPeer(peerId) as? TelegramUser {
updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedEmojiStatus(file.flatMap({ PeerEmojiStatus(content: .emoji(fileId: $0.fileId.id), expirationDate: expirationDate) }))], update: { _, updated in
updated
})
}
}
|> ignoreValues
|> then(remoteApply)
}
public func updateAccountBusinessHours(businessHours: TelegramBusinessHours?) -> Signal<Never, NoError> {
let peerId = self.account.peerId
var flags: Int32 = 0
if businessHours != nil {
flags |= 1 << 0
}
let remoteApply: Signal<Never, NoError> = self.account.network.request(Api.functions.account.updateBusinessWorkHours(flags: flags, businessWorkHours: businessHours?.apiBusinessHours))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
return self.account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
let current = current as? CachedUserData ?? CachedUserData()
return current.withUpdatedBusinessHours(businessHours)
})
}
|> ignoreValues
|> then(remoteApply)
}
public func updateAccountBusinessLocation(businessLocation: TelegramBusinessLocation?) -> Signal<Never, NoError> {
let peerId = self.account.peerId
var flags: Int32 = 0
var inputGeoPoint: Api.InputGeoPoint?
var inputAddress: String?
if let businessLocation {
flags |= 1 << 0
inputAddress = businessLocation.address
inputGeoPoint = businessLocation.coordinates?.apiInputGeoPoint
if inputGeoPoint != nil {
flags |= 1 << 1
}
}
let remoteApply: Signal<Never, NoError> = self.account.network.request(Api.functions.account.updateBusinessLocation(flags: flags, geoPoint: inputGeoPoint, address: inputAddress))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
return self.account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
let current = current as? CachedUserData ?? CachedUserData()
return current.withUpdatedBusinessLocation(businessLocation)
})
}
|> ignoreValues
|> then(remoteApply)
}
public func shortcutMessageList(onlyRemote: Bool) -> Signal<ShortcutMessageList, NoError> {
return _internal_shortcutMessageList(account: self.account, onlyRemote: onlyRemote)
}
public func keepShortcutMessageListUpdated() -> Signal<Never, NoError> {
return _internal_keepShortcutMessagesUpdated(account: self.account)
}
public func editMessageShortcut(id: Int32, shortcut: String) {
let _ = _internal_editMessageShortcut(account: self.account, id: id, shortcut: shortcut).startStandalone()
}
public func deleteMessageShortcuts(ids: [Int32]) {
let _ = _internal_deleteMessageShortcuts(account: self.account, ids: ids).startStandalone()
}
public func reorderMessageShortcuts(ids: [Int32], completion: @escaping () -> Void) {
let _ = _internal_reorderMessageShortcuts(account: self.account, ids: ids, localCompletion: completion).startStandalone()
}
public func sendMessageShortcut(peerId: EnginePeer.Id, id: Int32) {
let _ = _internal_sendMessageShortcut(account: self.account, peerId: peerId, id: id).startStandalone()
}
public func cachedTimeZoneList() -> Signal<TimeZoneList?, NoError> {
return _internal_cachedTimeZoneList(account: self.account)
}
public func keepCachedTimeZoneListUpdated() -> Signal<Never, NoError> {
return _internal_keepCachedTimeZoneListUpdated(account: self.account)
}
public func updateBusinessGreetingMessage(greetingMessage: TelegramBusinessGreetingMessage?) -> Signal<Never, NoError> {
return _internal_updateBusinessGreetingMessage(account: self.account, greetingMessage: greetingMessage)
}
public func updateBusinessAwayMessage(awayMessage: TelegramBusinessAwayMessage?) -> Signal<Never, NoError> {
return _internal_updateBusinessAwayMessage(account: self.account, awayMessage: awayMessage)
}
public func setAccountConnectedBot(bot: TelegramAccountConnectedBot?) -> Signal<Never, NoError> {
return _internal_setAccountConnectedBot(account: self.account, bot: bot)
}
public func updateBusinessIntro(intro: TelegramBusinessIntro?) -> Signal<Never, NoError> {
return _internal_updateBusinessIntro(account: self.account, intro: intro)
}
public func createBusinessChatLink(message: String, entities: [MessageTextEntity], title: String?) -> Signal<TelegramBusinessChatLinks.Link, AddBusinessChatLinkError> {
return _internal_createBusinessChatLink(account: self.account, message: message, entities: entities, title: title)
}
public func editBusinessChatLink(url: String, message: String, entities: [MessageTextEntity], title: String?) -> Signal<TelegramBusinessChatLinks.Link, AddBusinessChatLinkError> {
return _internal_editBusinessChatLink(account: self.account, url: url, message: message, entities: entities, title: title)
}
public func deleteBusinessChatLink(url: String) -> Signal<Never, NoError> {
return _internal_deleteBusinessChatLink(account: self.account, url: url)
}
public func refreshBusinessChatLinks() -> Signal<Never, NoError> {
return _internal_refreshBusinessChatLinks(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId)
}
public func updatePersonalChannel(personalChannel: TelegramPersonalChannel?) -> Signal<Never, NoError> {
return _internal_updatePersonalChannel(account: self.account, personalChannel: personalChannel)
}
public func updateAdMessagesEnabled(enabled: Bool) -> Signal<Never, AdMessagesEnableError> {
return _internal_updateAdMessagesEnabled(account: self.account, enabled: enabled)
}
}
}
@@ -0,0 +1,66 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
public struct TermsOfServiceUpdate: Equatable {
public let id: String
public let text: String
public let entities: [MessageTextEntity]
public let ageConfirmation: Int32?
init(id: String, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?) {
self.id = id
self.text = text
self.entities = entities
self.ageConfirmation = ageConfirmation
}
}
extension TermsOfServiceUpdate {
init?(apiTermsOfService: Api.help.TermsOfService) {
switch apiTermsOfService {
case let .termsOfService(_, id, text, entities, minAgeConfirm):
let idData: String
switch id {
case let .dataJSON(data):
idData = data
}
self.init(id: idData, text: text, entities: messageTextEntitiesFromApiEntities(entities), ageConfirmation: minAgeConfirm)
}
}
}
func _internal_acceptTermsOfService(account: Account, id: String) -> Signal<Void, NoError> {
return account.network.request(Api.functions.help.acceptTermsOfService(id: .dataJSON(data: id)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { [weak account] _ -> Signal<Void, NoError> in
account?.stateManager.modifyTermsOfServiceUpdate({ _ in nil })
return .complete()
}
}
func managedTermsOfServiceUpdates(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
let poll = network.request(Api.functions.help.getTermsOfServiceUpdate())
|> retryRequest
|> mapToSignal { [weak stateManager] result -> Signal<Void, NoError> in
var updated: TermsOfServiceUpdate?
switch result {
case let .termsOfServiceUpdate(_, termsOfService):
updated = TermsOfServiceUpdate(apiTermsOfService: termsOfService)
case .termsOfServiceUpdateEmpty:
break
}
stateManager?.modifyTermsOfServiceUpdate { _ in
return updated
}
return .complete()
}
return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
@@ -0,0 +1,114 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func _internal_updateAccountPeerName(account: Account, firstName: String, lastName: String) -> Signal<Void, NoError> {
let accountPeerId = account.peerId
return account.network.request(Api.functions.account.updateProfile(flags: (1 << 0) | (1 << 1), firstName: firstName, lastName: lastName, about: nil))
|> map { result -> Api.User? in
return result
}
|> `catch` { _ in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
return account.postbox.transaction { transaction -> Void in
if let result = result {
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(transaction: transaction, chats: [], users: [result]))
}
}
}
}
public enum UpdateAboutError {
case generic
}
func _internal_updateAbout(account: Account, about: String?) -> Signal<Void, UpdateAboutError> {
return account.network.request(Api.functions.account.updateProfile(flags: about == nil ? 0 : (1 << 2), firstName: nil, lastName: nil, about: about))
|> mapError { _ -> UpdateAboutError in
return .generic
}
|> mapToSignal { apiUser -> Signal<Void, UpdateAboutError> in
return account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in
if let current = current as? CachedUserData {
return current.withUpdatedAbout(about)
} else {
return current
}
})
}
|> castError(UpdateAboutError.self)
}
}
public enum UpdateNameColor {
case preset(color: PeerNameColor, backgroundEmojiId: Int64?)
case collectible(PeerCollectibleColor)
}
public enum UpdateNameColorAndEmojiError {
case generic
}
func _internal_updateNameColorAndEmoji(account: Account, nameColor: UpdateNameColor, profileColor: PeerNameColor?, profileBackgroundEmojiId: Int64?) -> Signal<Void, UpdateNameColorAndEmojiError> {
return account.postbox.transaction { transaction -> Signal<Peer, NoError> in
guard let peer = transaction.getPeer(account.peerId) as? TelegramUser else {
return .complete()
}
var nameColorValue: PeerColor
var backgroundEmojiIdValue: Int64?
switch nameColor {
case let .preset(color, backgroundEmojiId):
nameColorValue = .preset(color)
backgroundEmojiIdValue = backgroundEmojiId
case let .collectible(collectibleColor):
nameColorValue = .collectible(collectibleColor)
backgroundEmojiIdValue = collectibleColor.backgroundEmojiId
}
updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedNameColor(nameColorValue).withUpdatedBackgroundEmojiId(backgroundEmojiIdValue).withUpdatedProfileColor(profileColor).withUpdatedProfileBackgroundEmojiId(profileBackgroundEmojiId)], update: { _, updated in
return updated
})
return .single(peer)
}
|> switchToLatest
|> castError(UpdateNameColorAndEmojiError.self)
|> mapToSignal { _ -> Signal<Void, UpdateNameColorAndEmojiError> in
let inputRepliesColor: Api.PeerColor
switch nameColor {
case let .preset(color, backgroundEmojiId):
var flags: Int32 = (1 << 0)
if let _ = backgroundEmojiId {
flags |= (1 << 1)
}
inputRepliesColor = .peerColor(flags: flags, color: color.rawValue, backgroundEmojiId: backgroundEmojiId)
case let .collectible(collectibleColor):
inputRepliesColor = .inputPeerColorCollectible(collectibleId: collectibleColor.collectibleId)
}
var flagsProfile: Int32 = 0
if let _ = profileColor {
flagsProfile |= (1 << 0)
}
if let _ = profileBackgroundEmojiId {
flagsProfile |= (1 << 1)
}
return combineLatest(
account.network.request(Api.functions.account.updateColor(flags: (1 << 2), color: inputRepliesColor)),
account.network.request(Api.functions.account.updateColor(flags: (1 << 1) | (1 << 2), color: .peerColor(flags: flagsProfile, color: profileColor?.rawValue ?? 0, backgroundEmojiId: profileBackgroundEmojiId)))
)
|> mapError { _ -> UpdateNameColorAndEmojiError in
return .generic
}
|> mapToSignal { _, _ -> Signal<Void, UpdateNameColorAndEmojiError> in
return .complete()
}
}
}
@@ -0,0 +1,174 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
public struct AuthTransferExportedToken {
public let value: Data
public let validUntil: Int32
}
public enum ExportAuthTransferTokenError {
case generic
case limitExceeded
}
public enum ExportAuthTransferTokenResult {
case displayToken(AuthTransferExportedToken)
case changeAccountAndRetry(UnauthorizedAccount)
case loggedIn
case passwordRequested(UnauthorizedAccount)
}
func _internal_exportAuthTransferToken(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, otherAccountUserIds: [PeerId.Id], syncContacts: Bool) -> Signal<ExportAuthTransferTokenResult, ExportAuthTransferTokenError> {
return account.network.request(Api.functions.auth.exportLoginToken(apiId: account.networkArguments.apiId, apiHash: account.networkArguments.apiHash, exceptIds: otherAccountUserIds.map({ $0._internalGetInt64Value() })))
|> map(Optional.init)
|> `catch` { error -> Signal<Api.auth.LoginToken?, ExportAuthTransferTokenError> in
if error.errorDescription == "SESSION_PASSWORD_NEEDED" {
return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false)
|> mapError { error -> ExportAuthTransferTokenError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else {
return .generic
}
}
|> mapToSignal { result -> Signal<Api.auth.LoginToken?, ExportAuthTransferTokenError> in
switch result {
case let .password(_, _, _, _, hint, _, _, _, _, _, _):
return account.postbox.transaction { transaction -> Api.auth.LoginToken? in
transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint ?? "", number: nil, code: nil, suggestReset: false, syncContacts: syncContacts)))
return nil
}
|> castError(ExportAuthTransferTokenError.self)
}
}
} else {
return .fail(.generic)
}
}
|> mapToSignal { result -> Signal<ExportAuthTransferTokenResult, ExportAuthTransferTokenError> in
guard let result = result else {
return .single(.passwordRequested(account))
}
switch result {
case let .loginToken(expires, token):
return .single(.displayToken(AuthTransferExportedToken(value: token.makeData(), validUntil: expires)))
case let .loginTokenMigrateTo(dcId, token):
let updatedAccount = account.changedMasterDatacenterId(accountManager: accountManager, masterDatacenterId: dcId)
return updatedAccount
|> castError(ExportAuthTransferTokenError.self)
|> mapToSignal { updatedAccount -> Signal<ExportAuthTransferTokenResult, ExportAuthTransferTokenError> in
return updatedAccount.network.request(Api.functions.auth.importLoginToken(token: token))
|> map(Optional.init)
|> `catch` { error -> Signal<Api.auth.LoginToken?, ExportAuthTransferTokenError> in
if error.errorDescription == "SESSION_PASSWORD_NEEDED" {
return updatedAccount.network.request(Api.functions.account.getPassword(), automaticFloodWait: false)
|> mapError { error -> ExportAuthTransferTokenError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else {
return .generic
}
}
|> mapToSignal { result -> Signal<Api.auth.LoginToken?, ExportAuthTransferTokenError> in
switch result {
case let .password(_, _, _, _, hint, _, _, _, _, _, _):
return updatedAccount.postbox.transaction { transaction -> Api.auth.LoginToken? in
transaction.setState(UnauthorizedAccountState(isTestingEnvironment: updatedAccount.testingEnvironment, masterDatacenterId: updatedAccount.masterDatacenterId, contents: .passwordEntry(hint: hint ?? "", number: nil, code: nil, suggestReset: false, syncContacts: syncContacts)))
return nil
}
|> castError(ExportAuthTransferTokenError.self)
}
}
} else {
return .fail(.generic)
}
}
|> mapToSignal { result -> Signal<ExportAuthTransferTokenResult, ExportAuthTransferTokenError> in
guard let result = result else {
return .single(.passwordRequested(updatedAccount))
}
switch result {
case let .loginTokenSuccess(authorization):
switch authorization {
case let .authorization(_, _, _, futureAuthToken, user):
if let futureAuthToken = futureAuthToken {
storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData())
}
return updatedAccount.postbox.transaction { transaction -> Signal<ExportAuthTransferTokenResult, ExportAuthTransferTokenError> in
let user = TelegramUser(user: user)
let state = AuthorizedAccountState(isTestingEnvironment: updatedAccount.testingEnvironment, masterDatacenterId: updatedAccount.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: updatedAccount.networkArguments.appVersion, syncContacts: syncContacts)
transaction.setState(state)
return accountManager.transaction { transaction -> ExportAuthTransferTokenResult in
switchToAuthorizedAccount(transaction: transaction, account: updatedAccount, isSupportUser: false)
return .loggedIn
}
|> castError(ExportAuthTransferTokenError.self)
}
|> castError(ExportAuthTransferTokenError.self)
|> switchToLatest
default:
return .fail(.generic)
}
default:
return .single(.changeAccountAndRetry(updatedAccount))
}
}
}
case let .loginTokenSuccess(authorization):
switch authorization {
case let .authorization(_, _, _, futureAuthToken, user):
if let futureAuthToken = futureAuthToken {
storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData())
}
return account.postbox.transaction { transaction -> Signal<ExportAuthTransferTokenResult, ExportAuthTransferTokenError> in
let user = TelegramUser(user: user)
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
transaction.setState(state)
return accountManager.transaction { transaction -> ExportAuthTransferTokenResult in
switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: false)
return .loggedIn
}
|> castError(ExportAuthTransferTokenError.self)
}
|> castError(ExportAuthTransferTokenError.self)
|> switchToLatest
case .authorizationSignUpRequired:
return .fail(.generic)
}
}
}
}
public enum ApproveAuthTransferTokenError {
case generic
case invalid
case expired
case alreadyAccepted
}
public func approveAuthTransferToken(account: Account, token: Data, activeSessionsContext: ActiveSessionsContext) -> Signal<RecentAccountSession, ApproveAuthTransferTokenError> {
return account.network.request(Api.functions.auth.acceptLoginToken(token: Buffer(data: token)))
|> mapError { error -> ApproveAuthTransferTokenError in
switch error.errorDescription {
case "AUTH_TOKEN_INVALID":
return .invalid
case "AUTH_TOKEN_EXPIRED":
return .expired
case "AUTH_TOKEN_ALREADY_ACCEPTED":
return .alreadyAccepted
default:
return .generic
}
}
|> mapToSignal { authorization -> Signal<RecentAccountSession, ApproveAuthTransferTokenError> in
let session = RecentAccountSession(apiAuthorization: authorization)
activeSessionsContext.addSession(session)
return .single(session)
}
}
@@ -0,0 +1,87 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
public struct CancelAccountResetData: Equatable {
public let type: SentAuthorizationCodeType
public let hash: String
public let timeout: Int32?
public let nextType: AuthorizationCodeNextType?
}
public enum RequestCancelAccountResetDataError {
case limitExceeded
case generic
}
func _internal_requestCancelAccountResetData(network: Network, hash: String) -> Signal<CancelAccountResetData, RequestCancelAccountResetDataError> {
return network.request(Api.functions.account.sendConfirmPhoneCode(hash: hash, settings: .codeSettings(flags: 0, logoutTokens: nil, token: nil, appSandbox: nil)), automaticFloodWait: false)
|> mapError { error -> RequestCancelAccountResetDataError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else {
return .generic
}
}
|> mapToSignal { sentCode -> Signal<CancelAccountResetData, RequestCancelAccountResetDataError> in
switch sentCode {
case let .sentCode(_, type, phoneCodeHash, nextType, timeout):
var parsedNextType: AuthorizationCodeNextType?
if let nextType = nextType {
parsedNextType = AuthorizationCodeNextType(apiType: nextType)
}
return .single(CancelAccountResetData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType))
case .sentCodeSuccess, .sentCodePaymentRequired:
return .never()
}
}
}
func _internal_requestNextCancelAccountResetOption(network: Network, phoneNumber: String, phoneCodeHash: String) -> Signal<CancelAccountResetData, RequestCancelAccountResetDataError> {
return network.request(Api.functions.auth.resendCode(flags: 0, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, reason: nil), automaticFloodWait: false)
|> mapError { error -> RequestCancelAccountResetDataError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else {
return .generic
}
}
|> mapToSignal { sentCode -> Signal<CancelAccountResetData, RequestCancelAccountResetDataError> in
switch sentCode {
case let .sentCode(_, type, phoneCodeHash, nextType, timeout):
var parsedNextType: AuthorizationCodeNextType?
if let nextType = nextType {
parsedNextType = AuthorizationCodeNextType(apiType: nextType)
}
return .single(CancelAccountResetData(type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType))
case .sentCodeSuccess, .sentCodePaymentRequired:
return .never()
}
}
}
public enum CancelAccountResetError {
case generic
case invalidCode
case codeExpired
case limitExceeded
}
func _internal_requestCancelAccountReset(network: Network, phoneCodeHash: String, phoneCode: String) -> Signal<Never, CancelAccountResetError> {
return network.request(Api.functions.account.confirmPhone(phoneCodeHash: phoneCodeHash, phoneCode: phoneCode))
|> mapError { error -> CancelAccountResetError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "PHONE_CODE_INVALID" {
return .invalidCode
} else if error.errorDescription == "PHONE_CODE_EXPIRED" {
return .codeExpired
} else {
return .generic
}
}
|> ignoreValues
}
@@ -0,0 +1,58 @@
import Foundation
import SwiftSignalKit
import MtProtoKit
import TelegramApi
public enum ConfirmTwoStepRecoveryEmailError {
case invalidEmail
case invalidCode
case flood
case expired
case generic
}
func _internal_confirmTwoStepRecoveryEmail(network: Network, code: String) -> Signal<Never, ConfirmTwoStepRecoveryEmailError> {
return network.request(Api.functions.account.confirmPasswordEmail(code: code), automaticFloodWait: false)
|> mapError { error -> ConfirmTwoStepRecoveryEmailError in
if error.errorDescription == "EMAIL_INVALID" {
return .invalidEmail
} else if error.errorDescription == "CODE_INVALID" {
return .invalidCode
} else if error.errorDescription == "EMAIL_HASH_EXPIRED" {
return .expired
} else if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .flood
}
return .generic
}
|> ignoreValues
}
public enum ResendTwoStepRecoveryEmailError {
case flood
case generic
}
func _internal_resendTwoStepRecoveryEmail(network: Network) -> Signal<Never, ResendTwoStepRecoveryEmailError> {
return network.request(Api.functions.account.resendPasswordEmail(), automaticFloodWait: false)
|> mapError { error -> ResendTwoStepRecoveryEmailError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .flood
}
return .generic
}
|> ignoreValues
}
public enum CancelTwoStepRecoveryEmailError {
case generic
}
func _internal_cancelTwoStepRecoveryEmail(network: Network) -> Signal<Never, CancelTwoStepRecoveryEmailError> {
return network.request(Api.functions.account.cancelPasswordEmail(), automaticFloodWait: false)
|> mapError { _ -> CancelTwoStepRecoveryEmailError in
return .generic
}
|> ignoreValues
}
@@ -0,0 +1,284 @@
import SwiftSignalKit
import Postbox
import TelegramApi
import MtProtoKit
public enum TelegramEngineAuthorizationState {
case unauthorized(UnauthorizedAccountState)
case authorized
}
public extension TelegramEngineUnauthorized {
final class Auth {
private let account: UnauthorizedAccount
init(account: UnauthorizedAccount) {
self.account = account
}
public func exportAuthTransferToken(accountManager: AccountManager<TelegramAccountManagerTypes>, otherAccountUserIds: [PeerId.Id], syncContacts: Bool) -> Signal<ExportAuthTransferTokenResult, ExportAuthTransferTokenError> {
return _internal_exportAuthTransferToken(accountManager: accountManager, account: self.account, otherAccountUserIds: otherAccountUserIds, syncContacts: syncContacts)
}
public func twoStepAuthData() -> Signal<TwoStepAuthData, MTRpcError> {
return _internal_twoStepAuthData(self.account.network)
}
public func test() -> Signal<Bool, String> {
return _internal_test(self.account.network)
}
public func updateTwoStepVerificationPassword(currentPassword: String?, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal<UpdateTwoStepVerificationPasswordResult, UpdateTwoStepVerificationPasswordError> {
return _internal_updateTwoStepVerificationPassword(network: self.account.network, currentPassword: currentPassword, updatedPassword: updatedPassword)
}
public func requestTwoStepVerificationPasswordRecoveryCode() -> Signal<String, RequestTwoStepVerificationPasswordRecoveryCodeError> {
return _internal_requestTwoStepVerificationPasswordRecoveryCode(network: self.account.network)
}
public func checkPasswordRecoveryCode(code: String) -> Signal<Never, PasswordRecoveryError> {
return _internal_checkPasswordRecoveryCode(network: self.account.network, code: code)
}
public func performPasswordRecovery(code: String, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal<RecoveredAccountData, PasswordRecoveryError> {
return _internal_performPasswordRecovery(network: self.account.network, code: code, updatedPassword: updatedPassword)
}
public func resendTwoStepRecoveryEmail() -> Signal<Never, ResendTwoStepRecoveryEmailError> {
return _internal_resendTwoStepRecoveryEmail(network: self.account.network)
}
public func uploadedPeerVideo(resource: MediaResource) -> Signal<UploadedPeerPhotoData, NoError> {
return _internal_uploadedPeerVideo(postbox: self.account.postbox, network: self.account.network, messageMediaPreuploadManager: nil, resource: resource)
}
public func reportMissingCode(phoneNumber: String, phoneCodeHash: String, mnc: String) -> Signal<Never, ReportMissingCodeError> {
return _internal_reportMissingCode(network: self.account.network, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, mnc: mnc)
}
public func requestPasskeyLoginData(apiId: Int32, apiHash: String) -> Signal<String?, NoError> {
return _internal_requestPasskeyLoginData(network: self.account.network, apiId: apiId, apiHash: apiHash)
}
public func state() -> Signal<TelegramEngineAuthorizationState?, NoError> {
return self.account.postbox.stateView()
|> map { view -> TelegramEngineAuthorizationState? in
if let state = view.state as? UnauthorizedAccountState {
return .unauthorized(state)
} else if let _ = view.state as? AuthorizedAccountState {
return .authorized
} else {
return nil
}
}
}
public func setState(state: UnauthorizedAccountState) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
transaction.setState(state)
}
|> ignoreValues
}
}
}
public enum DeleteAccountError {
case generic
}
public extension TelegramEngine {
final class Auth {
private let account: Account
init(account: Account) {
self.account = account
}
public func twoStepAuthData() -> Signal<TwoStepAuthData, MTRpcError> {
return _internal_twoStepAuthData(self.account.network)
}
public func updateTwoStepVerificationPassword(currentPassword: String?, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal<UpdateTwoStepVerificationPasswordResult, UpdateTwoStepVerificationPasswordError> {
return _internal_updateTwoStepVerificationPassword(network: self.account.network, currentPassword: currentPassword, updatedPassword: updatedPassword)
}
public func deleteAccount(reason: String, password: String?) -> Signal<Never, DeleteAccountError> {
let network = self.account.network
let passwordSignal: Signal<Api.InputCheckPasswordSRP?, DeleteAccountError>
if let password = password {
passwordSignal = _internal_twoStepAuthData(network)
|> mapError { _ -> DeleteAccountError in
return .generic
}
|> mapToSignal { authData -> Signal<Api.InputCheckPasswordSRP?, DeleteAccountError> in
if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData {
guard let kdfResult = passwordKDF(encryptionProvider: 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 .single(nil)
}
}
} else {
passwordSignal = .single(nil)
}
return passwordSignal
|> mapToSignal { password -> Signal<Never, DeleteAccountError> in
var flags: Int32 = 0
if let _ = password {
flags |= (1 << 0)
}
return self.account.network.request(Api.functions.account.deleteAccount(flags: flags, reason: reason, password: password))
|> mapError { _ -> DeleteAccountError in
return .generic
}
|> ignoreValues
}
}
public func updateTwoStepVerificationEmail(currentPassword: String, updatedEmail: String) -> Signal<UpdateTwoStepVerificationPasswordResult, UpdateTwoStepVerificationPasswordError> {
return _internal_updateTwoStepVerificationEmail(network: self.account.network, currentPassword: currentPassword, updatedEmail: updatedEmail)
}
public func confirmTwoStepRecoveryEmail(code: String) -> Signal<Never, ConfirmTwoStepRecoveryEmailError> {
return _internal_confirmTwoStepRecoveryEmail(network: self.account.network, code: code)
}
public func resendTwoStepRecoveryEmail() -> Signal<Never, ResendTwoStepRecoveryEmailError> {
return _internal_resendTwoStepRecoveryEmail(network: self.account.network)
}
public func cancelTwoStepRecoveryEmail() -> Signal<Never, CancelTwoStepRecoveryEmailError> {
return _internal_cancelTwoStepRecoveryEmail(network: self.account.network)
}
public func twoStepVerificationConfiguration() -> Signal<TwoStepVerificationConfiguration, NoError> {
return _internal_twoStepVerificationConfiguration(account: self.account)
}
public func requestTwoStepVerifiationSettings(password: String) -> Signal<TwoStepVerificationSettings, AuthorizationPasswordVerificationError> {
return _internal_requestTwoStepVerifiationSettings(network: self.account.network, password: password)
}
public func requestTwoStepVerificationPasswordRecoveryCode() -> Signal<String, RequestTwoStepVerificationPasswordRecoveryCodeError> {
return _internal_requestTwoStepVerificationPasswordRecoveryCode(network: self.account.network)
}
public func performPasswordRecovery(code: String, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal<RecoveredAccountData, PasswordRecoveryError> {
return _internal_performPasswordRecovery(network: self.account.network, code: code, updatedPassword: updatedPassword)
}
public func cachedTwoStepPasswordToken() -> Signal<TemporaryTwoStepPasswordToken?, NoError> {
return _internal_cachedTwoStepPasswordToken(postbox: self.account.postbox)
}
public func cacheTwoStepPasswordToken(token: TemporaryTwoStepPasswordToken?) -> Signal<Void, NoError> {
return _internal_cacheTwoStepPasswordToken(postbox: self.account.postbox, token: token)
}
public func requestTemporaryTwoStepPasswordToken(password: String, period: Int32, requiresBiometrics: Bool) -> Signal<TemporaryTwoStepPasswordToken, AuthorizationPasswordVerificationError> {
return _internal_requestTemporaryTwoStepPasswordToken(account: self.account, password: password, period: period, requiresBiometrics: requiresBiometrics)
}
public func checkPasswordRecoveryCode(code: String) -> Signal<Never, PasswordRecoveryError> {
return _internal_checkPasswordRecoveryCode(network: self.account.network, code: code)
}
public func requestTwoStepPasswordReset() -> Signal<RequestTwoStepPasswordResetResult, NoError> {
return _internal_requestTwoStepPasswordReset(network: self.account.network)
}
public func declineTwoStepPasswordReset() -> Signal<Never, NoError> {
return _internal_declineTwoStepPasswordReset(network: self.account.network)
}
public func requestCancelAccountResetData(hash: String) -> Signal<CancelAccountResetData, RequestCancelAccountResetDataError> {
return _internal_requestCancelAccountResetData(network: self.account.network, hash: hash)
}
public func requestNextCancelAccountResetOption(phoneNumber: String, phoneCodeHash: String) -> Signal<CancelAccountResetData, RequestCancelAccountResetDataError> {
return _internal_requestNextCancelAccountResetOption(network: self.account.network, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash)
}
public func requestCancelAccountReset(phoneCodeHash: String, phoneCode: String) -> Signal<Never, CancelAccountResetError> {
return _internal_requestCancelAccountReset(network: self.account.network, phoneCodeHash: phoneCodeHash, phoneCode: phoneCode)
}
public func invalidateLoginCodes(codes: [String]) -> Signal<Never, NoError> {
return _internal_invalidateLoginCodes(network: self.account.network, codes: codes)
}
public func reportMissingCode(phoneNumber: String, phoneCodeHash: String, mnc: String) -> Signal<Never, ReportMissingCodeError> {
return _internal_reportMissingCode(network: self.account.network, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, mnc: mnc)
}
public func passkeysData() -> Signal<[TelegramPasskey], NoError> {
return _internal_passkeysData(network: self.account.network)
}
public func requestPasskeyRegistration() -> Signal<String?, NoError> {
return _internal_requestPasskeyRegistration(network: self.account.network)
}
public func requestCreatePasskey(id: String, clientData: String, attestationObject: Data) -> Signal<TelegramPasskey?, NoError> {
return _internal_requestCreatePasskey(network: self.account.network, id: id, clientData: clientData, attestationObject: attestationObject)
}
public func deletePasskey(id: String) -> Signal<Never, NoError> {
return _internal_deletePasskey(network: self.account.network, id: id)
}
}
}
public extension SomeTelegramEngine {
final class Auth {
private let engine: SomeTelegramEngine
init(engine: SomeTelegramEngine) {
self.engine = engine
}
public func twoStepAuthData() -> Signal<TwoStepAuthData, MTRpcError> {
switch self.engine {
case let .authorized(engine):
return engine.auth.twoStepAuthData()
case let .unauthorized(engine):
return engine.auth.twoStepAuthData()
}
}
public func updateTwoStepVerificationPassword(currentPassword: String?, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal<UpdateTwoStepVerificationPasswordResult, UpdateTwoStepVerificationPasswordError> {
switch self.engine {
case let .authorized(engine):
return engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: updatedPassword)
case let .unauthorized(engine):
return engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: updatedPassword)
}
}
public func requestTwoStepVerificationPasswordRecoveryCode() -> Signal<String, RequestTwoStepVerificationPasswordRecoveryCodeError> {
switch self.engine {
case let .authorized(engine):
return engine.auth.requestTwoStepVerificationPasswordRecoveryCode()
case let .unauthorized(engine):
return engine.auth.requestTwoStepVerificationPasswordRecoveryCode()
}
}
public func checkPasswordRecoveryCode(code: String) -> Signal<Never, PasswordRecoveryError> {
switch self.engine {
case let .authorized(engine):
return engine.auth.checkPasswordRecoveryCode(code: code)
case let .unauthorized(engine):
return engine.auth.checkPasswordRecoveryCode(code: code)
}
}
}
var auth: Auth {
return Auth(engine: self)
}
}
@@ -0,0 +1,449 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum TwoStepVerificationConfiguration {
case notSet(pendingEmail: TwoStepVerificationPendingEmail?)
case set(hint: String, hasRecoveryEmail: Bool, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool, pendingResetTimestamp: Int32?)
}
func _internal_twoStepVerificationConfiguration(account: Account) -> Signal<TwoStepVerificationConfiguration, NoError> {
return account.network.request(Api.functions.account.getPassword())
|> retryRequest
|> map { result -> TwoStepVerificationConfiguration in
switch result {
case let .password(flags, currentAlgo, _, _, hint, emailUnconfirmedPattern, _, _, _, pendingResetDate, _):
if currentAlgo != nil {
return .set(hint: hint ?? "", hasRecoveryEmail: (flags & (1 << 0)) != 0, pendingEmail: emailUnconfirmedPattern.flatMap({ TwoStepVerificationPendingEmail(pattern: $0, codeLength: nil) }), hasSecureValues: (flags & (1 << 1)) != 0, pendingResetTimestamp: pendingResetDate)
} else {
return .notSet(pendingEmail: emailUnconfirmedPattern.flatMap({ TwoStepVerificationPendingEmail(pattern: $0, codeLength: nil) }))
}
}
}
}
public struct TwoStepVerificationSecureSecret {
public let data: Data
public let derivation: TwoStepSecurePasswordDerivation
public let id: Int64
}
public struct TwoStepVerificationSettings {
public let email: String
public let secureSecret: TwoStepVerificationSecureSecret?
}
func _internal_requestTwoStepVerifiationSettings(network: Network, password: String) -> Signal<TwoStepVerificationSettings, AuthorizationPasswordVerificationError> {
return _internal_twoStepAuthData(network)
|> mapError { error -> AuthorizationPasswordVerificationError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "PASSWORD_HASH_INVALID" {
return .invalidPassword
} else {
return .generic
}
}
|> mapToSignal { authData -> Signal<TwoStepVerificationSettings, AuthorizationPasswordVerificationError> in
guard let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData else {
return .fail(.generic)
}
guard let kdfResult = passwordKDF(encryptionProvider: network.encryptionProvider, password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else {
return .fail(.generic)
}
return network.request(Api.functions.account.getPasswordSettings(password: .inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1))), automaticFloodWait: false)
|> mapError { error -> AuthorizationPasswordVerificationError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "PASSWORD_HASH_INVALID" {
return .invalidPassword
} else {
return .generic
}
}
|> mapToSignal { result -> Signal<TwoStepVerificationSettings, AuthorizationPasswordVerificationError> in
switch result {
case let .passwordSettings(_, email, secureSettings):
var parsedSecureSecret: TwoStepVerificationSecureSecret?
if let secureSettings = secureSettings {
switch secureSettings {
case let .secureSecretSettings(secureAlgo, secureSecret, secureSecretId):
if secureSecret.size != 32 {
return .fail(.generic)
}
parsedSecureSecret = TwoStepVerificationSecureSecret(data: secureSecret.makeData(), derivation: TwoStepSecurePasswordDerivation(secureAlgo), id: secureSecretId)
}
}
return .single(TwoStepVerificationSettings(email: email ?? "", secureSecret: parsedSecureSecret))
}
}
}
}
public enum UpdateTwoStepVerificationPasswordError {
case generic
case invalidEmail
}
public struct TwoStepVerificationPendingEmail: Equatable {
public let pattern: String
public let codeLength: Int32?
public init(pattern: String, codeLength: Int32?) {
self.pattern = pattern
self.codeLength = codeLength
}
}
public enum UpdateTwoStepVerificationPasswordResult {
case none
case password(password: String, pendingEmail: TwoStepVerificationPendingEmail?)
}
public enum UpdatedTwoStepVerificationPassword {
case none
case password(password: String, hint: String, email: String?)
}
func _internal_updateTwoStepVerificationPassword(network: Network, currentPassword: String?, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal<UpdateTwoStepVerificationPasswordResult, UpdateTwoStepVerificationPasswordError> {
return _internal_twoStepAuthData(network)
|> mapError { _ -> UpdateTwoStepVerificationPasswordError in
return .generic
}
|> mapToSignal { authData -> Signal<TwoStepVerificationSecureSecret?, UpdateTwoStepVerificationPasswordError> in
if let _ = authData.currentPasswordDerivation {
return _internal_requestTwoStepVerifiationSettings(network: network, password: currentPassword ?? "")
|> mapError { _ -> UpdateTwoStepVerificationPasswordError in
return .generic
}
|> map { settings in
return settings.secureSecret
}
} else {
return .single(nil)
}
}
|> mapToSignal { secureSecret -> Signal<(TwoStepAuthData, TwoStepVerificationSecureSecret?), UpdateTwoStepVerificationPasswordError> in
return _internal_twoStepAuthData(network)
|> mapError { _ -> UpdateTwoStepVerificationPasswordError in
return .generic
}
|> map { authData -> (TwoStepAuthData, TwoStepVerificationSecureSecret?) in
return (authData, secureSecret)
}
}
|> mapToSignal { authData, secureSecret -> Signal<UpdateTwoStepVerificationPasswordResult, UpdateTwoStepVerificationPasswordError> in
let checkPassword: Api.InputCheckPasswordSRP
if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData {
if let kdfResult = passwordKDF(encryptionProvider: network.encryptionProvider, password: currentPassword ?? "", derivation: currentPasswordDerivation, srpSessionData: srpSessionData) {
checkPassword = .inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1))
} else {
return .fail(.generic)
}
} else {
checkPassword = .inputCheckPasswordEmpty
}
switch updatedPassword {
case .none:
var flags: Int32 = (1 << 1)
if authData.currentPasswordDerivation != nil {
flags |= (1 << 0)
}
return network.request(Api.functions.account.updatePasswordSettings(password: checkPassword, newSettings: .passwordInputSettings(flags: flags, newAlgo: .passwordKdfAlgoUnknown, newPasswordHash: Buffer(data: Data()), hint: "", email: "", newSecureSettings: nil)), automaticFloodWait: true)
|> mapError { _ -> UpdateTwoStepVerificationPasswordError in
return .generic
}
|> map { _ -> UpdateTwoStepVerificationPasswordResult in
return .none
}
case let .password(password, hint, email):
var flags: Int32 = 1 << 0
if email != nil {
flags |= (1 << 1)
}
guard let (updatedPasswordHash, updatedPasswordDerivation) = passwordUpdateKDF(encryptionProvider: network.encryptionProvider, password: password, derivation: authData.nextPasswordDerivation) else {
return .fail(.generic)
}
var updatedSecureSecret: TwoStepVerificationSecureSecret?
if let encryptedSecret = secureSecret {
if let decryptedSecret = decryptedSecureSecret(encryptedSecretData: encryptedSecret.data, password: currentPassword ?? "", derivation: encryptedSecret.derivation, id: encryptedSecret.id) {
if let (data, derivation, id) = encryptedSecureSecret(secretData: decryptedSecret, password: password, inputDerivation: authData.nextSecurePasswordDerivation) {
updatedSecureSecret = TwoStepVerificationSecureSecret(data: data, derivation: derivation, id: id)
} else {
return .fail(.generic)
}
} else {
return .fail(.generic)
}
}
var updatedSecureSettings: Api.SecureSecretSettings?
if let updatedSecureSecret = updatedSecureSecret {
flags |= 1 << 2
updatedSecureSettings = .secureSecretSettings(secureAlgo: updatedSecureSecret.derivation.apiAlgo, secureSecret: Buffer(data: updatedSecureSecret.data), secureSecretId: updatedSecureSecret.id)
}
return network.request(Api.functions.account.updatePasswordSettings(password: checkPassword, newSettings: Api.account.PasswordInputSettings.passwordInputSettings(flags: flags, newAlgo: updatedPasswordDerivation.apiAlgo, newPasswordHash: Buffer(data: updatedPasswordHash), hint: hint, email: email, newSecureSettings: updatedSecureSettings)), automaticFloodWait: false)
|> map { _ -> UpdateTwoStepVerificationPasswordResult in
return .password(password: password, pendingEmail: nil)
}
|> `catch` { error -> Signal<UpdateTwoStepVerificationPasswordResult, MTRpcError> in
if error.errorDescription.hasPrefix("EMAIL_UNCONFIRMED") {
var codeLength: Int32?
if error.errorDescription.hasPrefix("EMAIL_UNCONFIRMED_") {
if let value = Int32(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "EMAIL_UNCONFIRMED_".count)...]) {
codeLength = value
}
}
return _internal_twoStepAuthData(network)
|> map { result -> UpdateTwoStepVerificationPasswordResult in
return .password(password: password, pendingEmail: result.unconfirmedEmailPattern.flatMap({ TwoStepVerificationPendingEmail(pattern: $0, codeLength: codeLength) }))
}
} else {
return .fail(error)
}
}
|> mapError { error -> UpdateTwoStepVerificationPasswordError in
if error.errorDescription == "EMAIL_INVALID" {
return .invalidEmail
} else {
return .generic
}
}
}
}
}
enum UpdateTwoStepVerificationSecureSecretResult {
case success
}
enum UpdateTwoStepVerificationSecureSecretError {
case generic
}
func updateTwoStepVerificationSecureSecret(network: Network, password: String, secret: Data) -> Signal<UpdateTwoStepVerificationSecureSecretResult, UpdateTwoStepVerificationSecureSecretError> {
return _internal_twoStepAuthData(network)
|> mapError { _ -> UpdateTwoStepVerificationSecureSecretError in
return .generic
}
|> mapToSignal { authData -> Signal<UpdateTwoStepVerificationSecureSecretResult, UpdateTwoStepVerificationSecureSecretError> in
guard let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData else {
return .fail(.generic)
}
guard let kdfResult = passwordKDF(encryptionProvider: network.encryptionProvider, password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else {
return .fail(.generic)
}
let checkPassword: Api.InputCheckPasswordSRP = .inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1))
guard let (encryptedSecret, secretDerivation, secretId) = encryptedSecureSecret(secretData: secret, password: password, inputDerivation: authData.nextSecurePasswordDerivation) else {
return .fail(.generic)
}
let flags: Int32 = (1 << 2)
return network.request(Api.functions.account.updatePasswordSettings(password: checkPassword, newSettings: .passwordInputSettings(flags: flags, newAlgo: nil, newPasswordHash: nil, hint: "", email: "", newSecureSettings: .secureSecretSettings(secureAlgo: secretDerivation.apiAlgo, secureSecret: Buffer(data: encryptedSecret), secureSecretId: secretId))), automaticFloodWait: true)
|> mapError { _ -> UpdateTwoStepVerificationSecureSecretError in
return .generic
}
|> map { _ -> UpdateTwoStepVerificationSecureSecretResult in
return .success
}
}
}
func _internal_updateTwoStepVerificationEmail(network: Network, currentPassword: String, updatedEmail: String) -> Signal<UpdateTwoStepVerificationPasswordResult, UpdateTwoStepVerificationPasswordError> {
return _internal_twoStepAuthData(network)
|> mapError { _ -> UpdateTwoStepVerificationPasswordError in
return .generic
}
|> mapToSignal { authData -> Signal<UpdateTwoStepVerificationPasswordResult, UpdateTwoStepVerificationPasswordError> in
let checkPassword: Api.InputCheckPasswordSRP
if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData {
guard let kdfResult = passwordKDF(encryptionProvider: network.encryptionProvider, password: currentPassword, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else {
return .fail(.generic)
}
checkPassword = .inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1))
} else {
checkPassword = .inputCheckPasswordEmpty
}
let flags: Int32 = 1 << 1
return network.request(Api.functions.account.updatePasswordSettings(password: checkPassword, newSettings: Api.account.PasswordInputSettings.passwordInputSettings(flags: flags, newAlgo: nil, newPasswordHash: nil, hint: nil, email: updatedEmail, newSecureSettings: nil)), automaticFloodWait: false)
|> map { _ -> UpdateTwoStepVerificationPasswordResult in
return .password(password: currentPassword, pendingEmail: nil)
}
|> `catch` { error -> Signal<UpdateTwoStepVerificationPasswordResult, MTRpcError> in
if error.errorDescription.hasPrefix("EMAIL_UNCONFIRMED") {
return _internal_twoStepAuthData(network)
|> map { result -> UpdateTwoStepVerificationPasswordResult in
var codeLength: Int32?
if error.errorDescription.hasPrefix("EMAIL_UNCONFIRMED_") {
if let value = Int32(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "EMAIL_UNCONFIRMED_".count)...]) {
codeLength = value
}
}
return .password(password: currentPassword, pendingEmail: result.unconfirmedEmailPattern.flatMap({ TwoStepVerificationPendingEmail(pattern: $0, codeLength: codeLength) }))
}
} else {
return .fail(error)
}
}
|> mapError { error -> UpdateTwoStepVerificationPasswordError in
if error.errorDescription == "EMAIL_INVALID" {
return .invalidEmail
} else {
return .generic
}
}
}
}
public enum RequestTwoStepVerificationPasswordRecoveryCodeError {
case generic
case limitExceeded
}
func _internal_requestTwoStepVerificationPasswordRecoveryCode(network: Network) -> Signal<String, RequestTwoStepVerificationPasswordRecoveryCodeError> {
return network.request(Api.functions.auth.requestPasswordRecovery(), automaticFloodWait: false)
|> mapError { error -> RequestTwoStepVerificationPasswordRecoveryCodeError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription.hasPrefix("PASSWORD_RECOVERY_NA") {
return .generic
} else {
return .generic
}
}
|> map { result -> String in
switch result {
case let .passwordRecovery(emailPattern):
return emailPattern
}
}
}
public enum RecoverTwoStepVerificationPasswordError {
case generic
case codeExpired
case limitExceeded
case invalidCode
}
func _internal_cachedTwoStepPasswordToken(postbox: Postbox) -> Signal<TemporaryTwoStepPasswordToken?, NoError> {
return postbox.transaction { transaction -> TemporaryTwoStepPasswordToken? in
let key = ValueBoxKey(length: 1)
key.setUInt8(0, value: 0)
return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedTwoStepToken, key: key))?.get(TemporaryTwoStepPasswordToken.self)
}
}
func _internal_cacheTwoStepPasswordToken(postbox: Postbox, token: TemporaryTwoStepPasswordToken?) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 1)
key.setUInt8(0, value: 0)
if let token = token.flatMap(CodableEntry.init) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedTwoStepToken, key: key), entry: token)
} else {
transaction.removeItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedTwoStepToken, key: key))
}
}
}
func _internal_requestTemporaryTwoStepPasswordToken(account: Account, password: String, period: Int32, requiresBiometrics: Bool) -> Signal<TemporaryTwoStepPasswordToken, AuthorizationPasswordVerificationError> {
return _internal_twoStepAuthData(account.network)
|> mapToSignal { authData -> Signal<TemporaryTwoStepPasswordToken, MTRpcError> in
guard let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData else {
return .fail(MTRpcError(errorCode: 400, errorDescription: "NO_PASSWORD"))
}
guard let kdfResult = passwordKDF(encryptionProvider: account.network.encryptionProvider, password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else {
return .fail(MTRpcError(errorCode: 400, errorDescription: "KDF_ERROR"))
}
let checkPassword: Api.InputCheckPasswordSRP = .inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1))
return account.network.request(Api.functions.account.getTmpPassword(password: checkPassword, period: period), automaticFloodWait: false)
|> map { result -> TemporaryTwoStepPasswordToken in
switch result {
case let .tmpPassword(tmpPassword, validUntil):
return TemporaryTwoStepPasswordToken(token: tmpPassword.makeData(), validUntilDate: validUntil, requiresBiometrics: requiresBiometrics)
}
}
}
|> `catch` { error -> Signal<TemporaryTwoStepPasswordToken, AuthorizationPasswordVerificationError> in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .fail(.limitExceeded)
} else if error.errorDescription == "PASSWORD_HASH_INVALID" {
return .fail(.invalidPassword)
} else {
return .fail(.generic)
}
}
}
public enum RequestTwoStepPasswordResetResult {
public enum ErrorReason {
case generic
case limitExceeded(retryAtTimestamp: Int32?)
}
case done
case waitingForReset(resetAtTimestamp: Int32)
case declined
case error(reason: ErrorReason)
}
func _internal_requestTwoStepPasswordReset(network: Network) -> Signal<RequestTwoStepPasswordResetResult, NoError> {
return network.request(Api.functions.account.resetPassword(), automaticFloodWait: false)
|> map { result -> RequestTwoStepPasswordResetResult in
switch result {
case let .resetPasswordFailedWait(retryDate):
return .error(reason: .limitExceeded(retryAtTimestamp: retryDate))
case .resetPasswordOk:
return .done
case let .resetPasswordRequestedWait(untilDate):
return .waitingForReset(resetAtTimestamp: untilDate)
}
}
|> `catch` { error -> Signal<RequestTwoStepPasswordResetResult, NoError> in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .single(.error(reason: .limitExceeded(retryAtTimestamp: nil)))
} else if error.errorDescription.hasPrefix("RESET_WAIT_") {
if let remainingSeconds = Int32(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "RESET_WAIT_".count)...]) {
let timestamp = Int32(network.globalTime)
return .single(.waitingForReset(resetAtTimestamp: timestamp + remainingSeconds))
} else {
return .single(.error(reason: .generic))
}
} else if error.errorDescription.hasPrefix("RESET_PREVIOUS_WAIT_") {
if let remainingSeconds = Int32(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "RESET_PREVIOUS_WAIT_".count)...]) {
let timestamp = Int32(network.globalTime)
return .single(.waitingForReset(resetAtTimestamp: timestamp + remainingSeconds))
} else {
return .single(.error(reason: .generic))
}
} else if error.errorDescription == "RESET_PREVIOUS_DECLINE" {
return .single(.declined)
} else {
return .single(.error(reason: .generic))
}
}
}
func _internal_declineTwoStepPasswordReset(network: Network) -> Signal<Never, NoError> {
return network.request(Api.functions.account.declinePasswordReset())
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
import Foundation
import Postbox
import MtProtoKit
import SwiftSignalKit
import TelegramApi
func _internal_rateCall(account: Account, callId: CallId, starsCount: Int32, comment: String = "", userInitiated: Bool) -> Signal<Void, NoError> {
var flags: Int32 = 0
if userInitiated {
flags |= (1 << 0)
}
return account.network.request(Api.functions.phone.setCallRating(flags: flags, peer: Api.InputPhoneCall.inputPhoneCall(id: callId.id, accessHash: callId.accessHash), rating: starsCount, comment: comment))
|> retryRequest
|> map { _ in }
}
public enum SaveCallDebugLogResult {
case done
case sendFullLog
}
func _internal_saveCallDebugLog(network: Network, callId: CallId, log: String) -> Signal<SaveCallDebugLogResult, NoError> {
if log.count > 1024 * 16 {
return .complete()
}
return network.request(Api.functions.phone.saveCallDebug(peer: Api.InputPhoneCall.inputPhoneCall(id: callId.id, accessHash: callId.accessHash), debug: .dataJSON(data: log)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolTrue)
}
|> map { result -> SaveCallDebugLogResult in
switch result {
case .boolFalse:
return .sendFullLog
case .boolTrue:
return .done
}
}
}
func _internal_saveCompleteCallDebugLog(account: Account, callId: CallId, logPath: String) -> Signal<Never, NoError> {
let tempFile = TempBox.shared.tempFile(fileName: "log.txt.gz")
do {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: logPath), options: .mappedIfSafe) else {
Logger.shared.log("saveCompleteCallDebugLog", "Failed to open log file")
return .complete()
}
guard let gzippedData = MTGzip.compress(data) else {
Logger.shared.log("saveCompleteCallDebugLog", "Failed to compress log file")
return .complete()
}
guard let _ = try? gzippedData.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) else {
Logger.shared.log("saveCompleteCallDebugLog", "Failed to write compressed log file")
return .complete()
}
}
guard let size = fileSize(tempFile.path) else {
Logger.shared.log("saveCompleteCallDebugLog", "Could not get log file size")
return .complete()
}
return multipartUpload(network: account.network, postbox: account.postbox, source: .tempFile(tempFile), encrypt: false, tag: nil, hintFileSize: size, hintFileIsLarge: false, forceNoBigParts: true, useLargerParts: false)
|> mapToSignal { value -> Signal<Never, MultipartUploadError> in
switch value {
case .progress:
return .complete()
case let .inputFile(inputFile):
return account.network.request(Api.functions.phone.saveCallLog(peer: Api.InputPhoneCall.inputPhoneCall(id: callId.id, accessHash: callId.accessHash), file: inputFile))
|> mapError { _ -> MultipartUploadError in
return .generic
}
|> ignoreValues
case .inputSecretFile:
return .fail(.generic)
}
}
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
}
@@ -0,0 +1,199 @@
import SwiftSignalKit
import Postbox
import TelegramApi
import MtProtoKit
import Foundation
public struct EngineCallStreamState {
public struct Channel {
public var id: Int32
public var scale: Int32
public var latestTimestamp: Int64
}
public var channels: [Channel]
}
public extension TelegramEngine {
final class Calls {
private let account: Account
init(account: Account) {
self.account = account
}
public func rateCall(callId: CallId, starsCount: Int32, comment: String = "", userInitiated: Bool) -> Signal<Void, NoError> {
return _internal_rateCall(account: self.account, callId: callId, starsCount: starsCount, comment: comment, userInitiated: userInitiated)
}
public func saveCallDebugLog(callId: CallId, log: String) -> Signal<SaveCallDebugLogResult, NoError> {
return _internal_saveCallDebugLog(network: self.account.network, callId: callId, log: log)
}
public func saveCompleteCallDebugLog(callId: CallId, logPath: String) -> Signal<Never, NoError> {
return _internal_saveCompleteCallDebugLog(account: self.account, callId: callId, logPath: logPath)
}
public func getCurrentGroupCall(reference: InternalGroupCallReference, peerId: PeerId? = nil) -> Signal<GroupCallSummary?, GetCurrentGroupCallError> {
return _internal_getCurrentGroupCall(account: self.account, reference: reference, peerId: peerId)
}
public func createGroupCall(peerId: PeerId, title: String?, scheduleDate: Int32?, isExternalStream: Bool) -> Signal<GroupCallInfo, CreateGroupCallError> {
return _internal_createGroupCall(account: self.account, peerId: peerId, title: title, scheduleDate: scheduleDate, isExternalStream: isExternalStream)
}
public func startScheduledGroupCall(peerId: PeerId, callId: Int64, accessHash: Int64) -> Signal<GroupCallInfo, StartScheduledGroupCallError> {
return _internal_startScheduledGroupCall(account: self.account, peerId: peerId, callId: callId, accessHash: accessHash)
}
public func toggleScheduledGroupCallSubscription(peerId: PeerId, reference: InternalGroupCallReference, subscribe: Bool) -> Signal<Void, ToggleScheduledGroupCallSubscriptionError> {
return _internal_toggleScheduledGroupCallSubscription(account: self.account, peerId: peerId, reference: reference, subscribe: subscribe)
}
public func updateGroupCallJoinAsPeer(peerId: PeerId, joinAs: PeerId) -> Signal<Never, UpdateGroupCallJoinAsPeerError> {
return _internal_updateGroupCallJoinAsPeer(account: self.account, peerId: peerId, joinAs: joinAs)
}
public func getGroupCallParticipants(reference: InternalGroupCallReference, offset: String, ssrcs: [UInt32], limit: Int32, sortAscending: Bool?) -> Signal<GroupCallParticipantsContext.State, GetGroupCallParticipantsError> {
return _internal_getGroupCallParticipants(account: self.account, reference: reference, offset: offset, ssrcs: ssrcs, limit: limit, sortAscending: sortAscending, isStream: false)
}
public func joinGroupCall(peerId: PeerId?, joinAs: PeerId?, callId: Int64, reference: InternalGroupCallReference, isStream: Bool, streamPeerId: PeerId?, preferMuted: Bool, joinPayload: String, peerAdminIds: Signal<[PeerId], NoError>, inviteHash: String? = nil, generateE2E: ((Data?) -> JoinGroupCallE2E?)?) -> Signal<JoinGroupCallResult, JoinGroupCallError> {
return _internal_joinGroupCall(account: self.account, peerId: peerId, joinAs: joinAs, callId: callId, reference: reference, isStream: isStream, streamPeerId: streamPeerId, preferMuted: preferMuted, joinPayload: joinPayload, peerAdminIds: peerAdminIds, inviteHash: inviteHash, generateE2E: generateE2E)
}
public func joinGroupCallAsScreencast(callId: Int64, accessHash: Int64, joinPayload: String) -> Signal<JoinGroupCallAsScreencastResult, JoinGroupCallError> {
return _internal_joinGroupCallAsScreencast(account: self.account, callId: callId, accessHash: accessHash, joinPayload: joinPayload)
}
public func leaveGroupCallAsScreencast(callId: Int64, accessHash: Int64) -> Signal<Never, LeaveGroupCallAsScreencastError> {
return _internal_leaveGroupCallAsScreencast(account: self.account, callId: callId, accessHash: accessHash)
}
public func leaveGroupCall(callId: Int64, accessHash: Int64, source: UInt32) -> Signal<Never, LeaveGroupCallError> {
return _internal_leaveGroupCall(account: self.account, callId: callId, accessHash: accessHash, source: source)
}
public func stopGroupCall(peerId: PeerId?, callId: Int64, accessHash: Int64) -> Signal<Never, StopGroupCallError> {
return _internal_stopGroupCall(account: self.account, peerId: peerId, callId: callId, accessHash: accessHash)
}
public func checkGroupCall(callId: Int64, accessHash: Int64, ssrcs: [UInt32]) -> Signal<[UInt32], NoError> {
return _internal_checkGroupCall(account: account, callId: callId, accessHash: accessHash, ssrcs: ssrcs)
}
public func inviteToGroupCall(callId: Int64, accessHash: Int64, peerId: PeerId) -> Signal<Never, InviteToGroupCallError> {
return _internal_inviteToGroupCall(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId)
}
public func groupCallInviteLinks(reference: InternalGroupCallReference, isConference: Bool) -> Signal<GroupCallInviteLinks?, NoError> {
return _internal_groupCallInviteLinks(account: self.account, reference: reference, isConference: isConference)
}
public func editGroupCallTitle(callId: Int64, accessHash: Int64, title: String) -> Signal<Never, EditGroupCallTitleError> {
return _internal_editGroupCallTitle(account: self.account, callId: callId, accessHash: accessHash, title: title)
}
public func createConferenceCall() -> Signal<EngineCreatedGroupCall, CreateConferenceCallError> {
return _internal_createConferenceCall(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId)
}
public func revokeConferenceInviteLink(reference: InternalGroupCallReference, link: String) -> Signal<GroupCallInviteLinks, RevokeConferenceInviteLinkError> {
return _internal_revokeConferenceInviteLink(account: self.account, reference: reference, link: link)
}
public func pollConferenceCallBlockchain(reference: InternalGroupCallReference, subChainId: Int, offset: Int, limit: Int) -> Signal<(blocks: [Data], nextOffset: Int)?, NoError> {
return _internal_pollConferenceCallBlockchain(network: self.account.network, reference: reference, subChainId: subChainId, offset: offset, limit: limit)
}
public func sendConferenceCallBroadcast(callId: Int64, accessHash: Int64, block: Data) -> Signal<Never, NoError> {
return _internal_sendConferenceCallBroadcast(account: self.account, callId: callId, accessHash: accessHash, block: block)
}
public func inviteConferenceCallParticipant(reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<EngineMessage.Id, InviteConferenceCallParticipantError> {
return _internal_inviteConferenceCallParticipant(account: self.account, reference: reference, peerId: peerId, isVideo: isVideo)
}
public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, mode: RemoveGroupCallBlockchainParticipantsMode, participantIds: [Int64], block: Data) -> Signal<RemoveGroupCallBlockchainParticipantsResult, NoError> {
return _internal_removeGroupCallBlockchainParticipants(account: self.account, callId: callId, accessHash: accessHash, mode: mode, participantIds: participantIds, block: block)
}
public func clearCachedGroupCallDisplayAsAvailablePeers(peerId: PeerId) -> Signal<Never, NoError> {
return _internal_clearCachedGroupCallDisplayAsAvailablePeers(account: self.account, peerId: peerId)
}
public func cachedGroupCallDisplayAsAvailablePeers(peerId: PeerId) -> Signal<[FoundPeer], NoError> {
return _internal_cachedGroupCallDisplayAsAvailablePeers(account: self.account, peerId: peerId)
}
public func updatedCurrentPeerGroupCall(peerId: PeerId) -> Signal<EngineGroupCallDescription?, NoError> {
return _internal_updatedCurrentPeerGroupCall(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, peerId: peerId)
|> map { activeCall -> EngineGroupCallDescription? in
return activeCall.flatMap(EngineGroupCallDescription.init)
}
}
public func getAudioBroadcastDataSource(callId: Int64, accessHash: Int64) -> Signal<AudioBroadcastDataSource?, NoError> {
return _internal_getAudioBroadcastDataSource(account: self.account, callId: callId, accessHash: accessHash)
}
public func getAudioBroadcastPart(dataSource: AudioBroadcastDataSource, callId: Int64, accessHash: Int64, timestampIdMilliseconds: Int64, durationMilliseconds: Int64) -> Signal<GetAudioBroadcastPartResult, NoError> {
return _internal_getAudioBroadcastPart(dataSource: dataSource, callId: callId, accessHash: accessHash, timestampIdMilliseconds: timestampIdMilliseconds, durationMilliseconds: durationMilliseconds)
}
public func getVideoBroadcastPart(dataSource: AudioBroadcastDataSource, callId: Int64, accessHash: Int64, timestampIdMilliseconds: Int64, durationMilliseconds: Int64, channelId: Int32, quality: Int32) -> Signal<GetAudioBroadcastPartResult, NoError> {
return _internal_getVideoBroadcastPart(dataSource: dataSource, callId: callId, accessHash: accessHash, timestampIdMilliseconds: timestampIdMilliseconds, durationMilliseconds: durationMilliseconds, channelId: channelId, quality: quality)
}
public func groupCall(peerId: PeerId?, myPeerId: PeerId, id: Int64, reference: InternalGroupCallReference, state: GroupCallParticipantsContext.State, previousServiceState: GroupCallParticipantsContext.ServiceState?, e2eContext: ConferenceCallE2EContext?) -> GroupCallParticipantsContext {
return GroupCallParticipantsContext(account: self.account, peerId: peerId, myPeerId: myPeerId, id: id, reference: reference, state: state, previousServiceState: previousServiceState, e2eContext: e2eContext)
}
public func serverTime() -> Signal<Int64, NoError> {
return self.account.network.currentGlobalTime
|> map { value -> Int64 in
return Int64(value * 1000.0)
}
|> take(1)
}
public func requestStreamState(dataSource: AudioBroadcastDataSource, callId: Int64, accessHash: Int64) -> Signal<EngineCallStreamState?, NoError> {
return dataSource.download.request(Api.functions.phone.getGroupCallStreamChannels(call: .inputGroupCall(id: callId, accessHash: accessHash)))
|> mapToSignal { result -> Signal<EngineCallStreamState?, MTRpcError> in
switch result {
case let .groupCallStreamChannels(channels):
let state = EngineCallStreamState(channels: channels.map { channel -> EngineCallStreamState.Channel in
switch channel {
case let .groupCallStreamChannel(channel, scale, lastTimestampMs):
return EngineCallStreamState.Channel(id: channel, scale: scale, latestTimestamp: lastTimestampMs)
}
})
return .single(state)
}
}
|> `catch` { _ -> Signal<EngineCallStreamState?, NoError> in
return .single(nil)
}
}
public func getGroupCallStreamCredentials(peerId: EnginePeer.Id, isLiveStream: Bool, revokePreviousCredentials: Bool) -> Signal<GroupCallStreamCredentials, GetGroupCallStreamCredentialsError> {
return _internal_getGroupCallStreamCredentials(account: self.account, peerId: peerId, isLiveStream: isLiveStream, revokePreviousCredentials: revokePreviousCredentials)
}
public func getGroupCallPersistentSettings(callId: Int64) -> Signal<CodableEntry?, NoError> {
return self.account.postbox.transaction { transaction -> CodableEntry? in
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: callId)
return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.groupCallPersistentSettings, key: key))
}
}
public func setGroupCallPersistentSettings(callId: Int64, value: CodableEntry) {
let _ = self.account.postbox.transaction({ transaction -> Void in
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: callId)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.groupCallPersistentSettings, key: key), entry: value)
}).startStandalone()
}
}
}
@@ -0,0 +1,204 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import CryptoUtils
private func md5(_ data: Data) -> Data {
return data.withUnsafeBytes { rawBytes -> Data in
let bytes = rawBytes.baseAddress!
return CryptoMD5(bytes, Int32(data.count))
}
}
private func updatedRemoteContactPeers(network: Network, hash: Int64) -> Signal<(AccumulatedPeers, Int32)?, NoError> {
return network.request(Api.functions.contacts.getContacts(hash: hash), automaticFloodWait: false)
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.contacts.Contacts?, NoError> in
return .single(nil)
}
|> map { result -> (AccumulatedPeers, Int32)? in
guard let result = result else {
return nil
}
switch result {
case .contactsNotModified:
return nil
case let .contacts(_, savedCount, users):
return (AccumulatedPeers(users: users), savedCount)
}
}
}
private func hashForCountAndIds(count: Int32, ids: [Int64]) -> Int64 {
var acc: UInt64 = 0
combineInt64Hash(&acc, with: UInt64(count))
for id in ids {
combineInt64Hash(&acc, with: UInt64(bitPattern: id))
}
return finalizeInt64Hash(acc)
}
func syncContactsOnce(network: Network, postbox: Postbox, accountPeerId: PeerId) -> Signal<Never, NoError> {
let initialContactPeerIdsHash = postbox.transaction { transaction -> Int64 in
let contactPeerIds = transaction.getContactPeerIds()
let totalCount = transaction.getRemoteContactCount()
let peerIds = Set(contactPeerIds.filter({ $0.namespace == Namespaces.Peer.CloudUser }))
return hashForCountAndIds(count: totalCount, ids: peerIds.map({ $0.id._internalGetInt64Value() }).sorted())
}
let updatedPeers = initialContactPeerIdsHash
|> mapToSignal { hash -> Signal<(AccumulatedPeers, Int32)?, NoError> in
return updatedRemoteContactPeers(network: network, hash: hash)
}
let appliedUpdatedPeers = updatedPeers
|> mapToSignal { peersAndPresences -> Signal<Never, NoError> in
if let (peers, totalCount) = peersAndPresences {
return postbox.transaction { transaction -> Signal<Void, NoError> in
let previousIds = transaction.getContactPeerIds()
let wasEmpty = previousIds.isEmpty
transaction.replaceRemoteContactCount(totalCount)
if wasEmpty {
let users = Array(peers.users.values)
var insertSignal: Signal<Void, NoError> = .complete()
for s in stride(from: 0, to: users.count, by: 500) {
let partUsers = Array(users[s ..< min(s + 500, users.count)])
let partSignal = postbox.transaction { transaction -> Void in
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: partUsers))
var updatedIds = transaction.getContactPeerIds()
updatedIds.formUnion(partUsers.map { $0.peerId })
transaction.replaceContactPeerIds(updatedIds)
}
|> delay(0.1, queue: Queue.concurrentDefaultQueue())
insertSignal = insertSignal |> then(partSignal)
}
return insertSignal
} else {
transaction.replaceContactPeerIds(Set(peers.users.keys))
return .complete()
}
}
|> switchToLatest
|> ignoreValues
} else {
return .complete()
}
}
return appliedUpdatedPeers
}
func _internal_deleteContactPeerInteractively(account: Account, peerId: PeerId) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
if let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) {
return account.network.request(Api.functions.contacts.deleteContacts(id: [inputUser]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return account.postbox.transaction { transaction -> Void in
if let user = peer as? TelegramUser {
_internal_updatePeerIsContact(transaction: transaction, user: user, isContact: false)
}
var peerIds = transaction.getContactPeerIds()
if peerIds.contains(peerId) {
peerIds.remove(peerId)
transaction.replaceContactPeerIds(peerIds)
}
}
}
|> ignoreValues
} else {
return .complete()
}
}
|> switchToLatest
}
func _internal_deleteContacts(account: Account, peerIds: [PeerId]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
let users = peerIds.compactMap { transaction.getPeer($0) }
let inputUsers: [Api.InputUser] = users.compactMap { apiInputUser($0) }
if !inputUsers.isEmpty {
return account.network.request(Api.functions.contacts.deleteContacts(id: inputUsers))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return account.postbox.transaction { transaction -> Void in
for user in users {
if let user = user as? TelegramUser {
_internal_updatePeerIsContact(transaction: transaction, user: user, isContact: false)
}
}
let updatedContactPeerIds = transaction.getContactPeerIds().filter { !peerIds.contains($0) }
transaction.replaceContactPeerIds(updatedContactPeerIds)
}
}
|> ignoreValues
} else {
return .complete()
}
}
|> switchToLatest
}
func _internal_deleteAllContacts(account: Account) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> [Api.InputUser] in
return transaction.getContactPeerIds().compactMap(transaction.getPeer).compactMap({ apiInputUser($0) }).compactMap({ $0 })
}
|> mapToSignal { users -> Signal<Never, NoError> in
let deleteContacts = account.network.request(Api.functions.contacts.deleteContacts(id: users))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
let deleteImported = account.network.request(Api.functions.contacts.resetSaved())
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
return combineLatest(deleteContacts, deleteImported)
|> mapToSignal { updates, _ -> Signal<Never, NoError> in
return account.postbox.transaction { transaction -> Void in
transaction.replaceContactPeerIds(Set())
transaction.clearDeviceContactImportInfoIdentifiers()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
account.restartContactManagement()
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
|> ignoreValues
}
}
}
func _internal_resetSavedContacts(network: Network) -> Signal<Void, NoError> {
return network.request(Api.functions.contacts.resetSaved())
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
@@ -0,0 +1,75 @@
import Foundation
import Postbox
public struct DeviceContactNormalizedPhoneNumber: Hashable, RawRepresentable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
public final class DeviceContactPhoneNumberValue: Equatable {
public let plain: String
public let normalized: DeviceContactNormalizedPhoneNumber
public init(plain: String, normalized: DeviceContactNormalizedPhoneNumber) {
self.plain = plain
self.normalized = normalized
}
public static func ==(lhs: DeviceContactPhoneNumberValue, rhs: DeviceContactPhoneNumberValue) -> Bool {
if lhs.plain != rhs.plain {
return false
}
if lhs.normalized != rhs.normalized {
return false
}
return true
}
}
public final class DeviceContactPhoneNumber: Equatable {
public let label: String
public let number: DeviceContactPhoneNumberValue
public init(label: String, number: DeviceContactPhoneNumberValue) {
self.label = label
self.number = number
}
public static func ==(lhs: DeviceContactPhoneNumber, rhs: DeviceContactPhoneNumber) -> Bool {
return lhs.label == rhs.label && lhs.number == rhs.number
}
}
public final class DeviceContact: Equatable {
public let id: String
public let firstName: String
public let lastName: String
public let phoneNumbers: [DeviceContactPhoneNumber]
public init(id: String, firstName: String, lastName: String, phoneNumbers: [DeviceContactPhoneNumber]) {
self.id = id
self.firstName = firstName
self.lastName = lastName
self.phoneNumbers = phoneNumbers
}
public static func ==(lhs: DeviceContact, rhs: DeviceContact) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.firstName != rhs.firstName {
return false
}
if lhs.lastName != rhs.lastName {
return false
}
if lhs.phoneNumbers != rhs.phoneNumbers {
return false
}
return true
}
}
@@ -0,0 +1,137 @@
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_importContact(account: Account, firstName: String, lastName: String, phoneNumber: String, noteText: String, noteEntities: [MessageTextEntity]) -> Signal<PeerId?, NoError> {
let accountPeerId = account.peerId
var flags: Int32 = 0
var note: Api.TextWithEntities?
if !noteText.isEmpty {
flags |= (1 << 1)
note = .textWithEntities(text: noteText, entities: apiEntitiesFromMessageTextEntities(noteEntities, associatedPeers: SimpleDictionary()))
}
let input = Api.InputContact.inputPhoneContact(flags: 0, clientId: 1, phone: phoneNumber, firstName: firstName, lastName: lastName, note: note)
return account.network.request(Api.functions.contacts.importContacts(contacts: [input]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.contacts.ImportedContacts?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<PeerId?, NoError> in
return account.postbox.transaction { transaction -> PeerId? in
if let result = result {
switch result {
case let .importedContacts(_, _, _, users):
if let first = users.first {
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
let peerId = first.peerId
var peerIds = transaction.getContactPeerIds()
if !peerIds.contains(peerId) {
peerIds.insert(peerId)
transaction.replaceContactPeerIds(peerIds)
}
if !noteText.isEmpty {
transaction.updatePeerCachedData(peerIds: [peerId], update: { peerId, cachedData in
(cachedData as? CachedUserData)?.withUpdatedNote(.init(text: noteText, entities: noteEntities))
})
}
return peerId
}
}
}
return nil
}
}
}
public enum AddContactError {
case generic
}
func _internal_addContactInteractively(account: Account, peerId: PeerId, firstName: String, lastName: String, phoneNumber: String, noteText: String, noteEntities: [MessageTextEntity], addToPrivacyExceptions: Bool) -> Signal<Never, AddContactError> {
let accountPeerId = account.peerId
return account.postbox.transaction { transaction -> (Api.InputUser, String)? in
if let user = transaction.getPeer(peerId) as? TelegramUser, let inputUser = apiInputUser(user) {
return (inputUser, user.phone == nil ? phoneNumber : "")
} else {
return nil
}
}
|> castError(AddContactError.self)
|> mapToSignal { inputUserAndPhone in
guard let (inputUser, phone) = inputUserAndPhone else {
return .fail(.generic)
}
var flags: Int32 = 0
if addToPrivacyExceptions {
flags |= (1 << 0)
}
var note: Api.TextWithEntities?
if !noteText.isEmpty {
flags |= (1 << 1)
note = .textWithEntities(text: noteText, entities: apiEntitiesFromMessageTextEntities(noteEntities, associatedPeers: SimpleDictionary()))
}
return account.network.request(Api.functions.contacts.addContact(flags: flags, id: inputUser, firstName: firstName, lastName: lastName, phone: phone, note: note))
|> mapError { _ -> AddContactError in
return .generic
}
|> mapToSignal { result -> Signal<Never, AddContactError> in
return account.postbox.transaction { transaction -> Void in
var peers = AccumulatedPeers()
switch result {
case let .updates(_, users, _, _, _):
peers = AccumulatedPeers(users: users)
case let .updatesCombined(_, users, _, _, _, _):
peers = AccumulatedPeers(users: users)
default:
break
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers)
var peerIds = transaction.getContactPeerIds()
if !peerIds.contains(peerId) {
peerIds.insert(peerId)
transaction.replaceContactPeerIds(peerIds)
}
if !noteText.isEmpty {
transaction.updatePeerCachedData(peerIds: [peerId], update: { peerId, cachedData in
(cachedData as? CachedUserData)?.withUpdatedNote(.init(text: noteText, entities: noteEntities))
})
}
account.stateManager.addUpdates(result)
}
|> castError(AddContactError.self)
|> ignoreValues
}
}
}
public enum AcceptAndShareContactError {
case generic
}
func _internal_acceptAndShareContact(account: Account, peerId: PeerId) -> Signal<Never, AcceptAndShareContactError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(peerId).flatMap(apiInputUser)
}
|> castError(AcceptAndShareContactError.self)
|> mapToSignal { inputUser -> Signal<Never, AcceptAndShareContactError> in
guard let inputUser = inputUser else {
return .fail(.generic)
}
return account.network.request(Api.functions.contacts.acceptContact(id: inputUser))
|> mapError { _ -> AcceptAndShareContactError in
return .generic
}
|> mapToSignal { updates -> Signal<Never, AcceptAndShareContactError> in
account.stateManager.addUpdates(updates)
return .complete()
}
}
}
@@ -0,0 +1,14 @@
public struct PhoneNumberWithLabel: Equatable {
public let label: String
public let number: String
public init(label: String, number: String) {
self.label = label
self.number = number
}
public static func ==(lhs: PhoneNumberWithLabel, rhs: PhoneNumberWithLabel) -> Bool {
return lhs.label == rhs.label && lhs.number == rhs.number
}
}
@@ -0,0 +1,66 @@
import Foundation
import Postbox
import SwiftSignalKit
private let phoneNumberKeyPrefix: ValueBoxKey = {
let result = ValueBoxKey(length: 1)
result.setInt8(0, value: 0)
return result
}()
enum TelegramDeviceContactImportIdentifier: Hashable, Comparable, Equatable {
case phoneNumber(DeviceContactNormalizedPhoneNumber)
init?(key: ValueBoxKey) {
if key.length < 2 {
return nil
}
switch key.getInt8(0) {
case 0:
guard let string = key.substringValue(1 ..< key.length) else {
return nil
}
self = .phoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: string))
default:
return nil
}
}
var key: ValueBoxKey {
switch self {
case let .phoneNumber(number):
let numberKey = ValueBoxKey(number.rawValue)
return phoneNumberKeyPrefix + numberKey
}
}
static func <(lhs: TelegramDeviceContactImportIdentifier, rhs: TelegramDeviceContactImportIdentifier) -> Bool {
switch lhs {
case let .phoneNumber(lhsNumber):
switch rhs {
case let .phoneNumber(rhsNumber):
return lhsNumber.rawValue < rhsNumber.rawValue
}
}
}
}
func _internal_deviceContactsImportedByCount(postbox: Postbox, contacts: [(String, [DeviceContactNormalizedPhoneNumber])]) -> Signal<[String: Int32], NoError> {
return postbox.transaction { transaction -> [String: Int32] in
var result: [String: Int32] = [:]
for (id, numbers) in contacts {
var maxCount: Int32 = 0
for number in numbers {
if let value = transaction.getDeviceContactImportInfo(TelegramDeviceContactImportIdentifier.phoneNumber(number).key) as? TelegramDeviceContactImportedData, case let .imported(_, importedByCount, _) = value {
maxCount = max(maxCount, importedByCount)
}
}
if maxCount != 0 {
result[id] = maxCount
}
}
return result
}
}
@@ -0,0 +1,144 @@
import Foundation
import SwiftSignalKit
import Postbox
public extension TelegramEngine {
final class Contacts {
private let account: Account
init(account: Account) {
self.account = account
}
public func deleteContactPeerInteractively(peerId: PeerId) -> Signal<Never, NoError> {
return _internal_deleteContactPeerInteractively(account: self.account, peerId: peerId)
}
public func deleteContacts(peerIds: [PeerId]) -> Signal<Never, NoError> {
return _internal_deleteContacts(account: self.account, peerIds: peerIds)
}
public func deleteAllContacts() -> Signal<Never, NoError> {
return _internal_deleteAllContacts(account: self.account)
}
public func resetSavedContacts() -> Signal<Void, NoError> {
return _internal_resetSavedContacts(network: self.account.network)
}
public func updateContactName(peerId: PeerId, firstName: String, lastName: String) -> Signal<Void, UpdateContactNameError> {
return _internal_updateContactName(account: self.account, peerId: peerId, firstName: firstName, lastName: lastName)
}
public func updateContactPhoto(peerId: PeerId, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> {
return _internal_updateContactPhoto(account: self.account, peerId: peerId, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: mode, mapResourceToAvatarSizes: mapResourceToAvatarSizes)
}
public func updateContactNote(peerId: PeerId, text: String, entities: [MessageTextEntity]) -> Signal<Never, UpdateContactNoteError> {
return _internal_updateContactNote(account: self.account, peerId: peerId, text: text, entities: entities)
}
public func deviceContactsImportedByCount(contacts: [(String, [DeviceContactNormalizedPhoneNumber])]) -> Signal<[String: Int32], NoError> {
return _internal_deviceContactsImportedByCount(postbox: self.account.postbox, contacts: contacts)
}
public func importContact(firstName: String, lastName: String, phoneNumber: String, noteText: String, noteEntities: [MessageTextEntity]) -> Signal<PeerId?, NoError> {
return _internal_importContact(account: self.account, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, noteText: noteText, noteEntities: noteEntities)
}
public func addContactInteractively(peerId: PeerId, firstName: String, lastName: String, phoneNumber: String, noteText: String, noteEntities: [MessageTextEntity], addToPrivacyExceptions: Bool) -> Signal<Never, AddContactError> {
return _internal_addContactInteractively(account: self.account, peerId: peerId, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, noteText: noteText, noteEntities: noteEntities, addToPrivacyExceptions: addToPrivacyExceptions)
}
public func acceptAndShareContact(peerId: PeerId) -> Signal<Never, AcceptAndShareContactError> {
return _internal_acceptAndShareContact(account: self.account, peerId: peerId)
}
public func searchRemotePeers(query: String, scope: TelegramSearchPeersScope = .everywhere) -> Signal<([FoundPeer], [FoundPeer]), NoError> {
return _internal_searchPeers(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, query: query, scope: scope)
}
public func searchLocalPeers(query: String, scope: TelegramSearchPeersScope = .everywhere) -> Signal<[EngineRenderedPeer], NoError> {
return self.account.postbox.searchPeers(query: query)
|> map { peers in
switch scope {
case .everywhere:
return peers.map(EngineRenderedPeer.init)
case .channels:
return peers.filter { peer in
if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
return true
} else {
return false
}
}.map(EngineRenderedPeer.init)
case .groups:
return peers.filter { item in
if let channel = item.peer as? TelegramChannel, case .group = channel.info {
return true
} else if item.peer is TelegramGroup {
return true
} else {
return false
}
}.map(EngineRenderedPeer.init)
case .privateChats:
return peers.filter { item in
if item.peer is TelegramUser {
return true
} else {
return false
}
}.map(EngineRenderedPeer.init)
case .globalPosts:
return []
}
}
}
public func searchContacts(query: String) -> Signal<([EnginePeer], [EnginePeer.Id: EnginePeer.Presence]), NoError> {
return self.account.postbox.searchContacts(query: query)
|> map { peers, presences in
return (peers.map(EnginePeer.init), presences.mapValues(EnginePeer.Presence.init))
}
}
public func updateIsContactSynchronizationEnabled(isContactSynchronizationEnabled: Bool) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.contactsSettings, { current in
var settings = current?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings
settings.synchronizeContacts = isContactSynchronizationEnabled
return PreferencesEntry(settings)
})
}
|> ignoreValues
}
public func findPeerByLocalContactIdentifier(identifier: String) -> Signal<EnginePeer?, NoError> {
return self.account.postbox.transaction { transaction -> EnginePeer? in
var foundPeerId: PeerId?
transaction.enumerateDeviceContactImportInfoItems({ _, value in
if let value = value as? TelegramDeviceContactImportedData {
switch value {
case let .imported(data, _, peerId):
if data.localIdentifiers.contains(identifier) {
if let peerId = peerId {
foundPeerId = peerId
return false
}
}
default:
break
}
}
return true
})
if let foundPeerId = foundPeerId {
return transaction.getPeer(foundPeerId).flatMap(EnginePeer.init)
} else {
return nil
}
}
}
}
}
@@ -0,0 +1,58 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum UpdateContactNameError {
case generic
}
func _internal_updateContactName(account: Account, peerId: PeerId, firstName: String, lastName: String) -> Signal<Void, UpdateContactNameError> {
return account.postbox.transaction { transaction -> Signal<Void, UpdateContactNameError> in
if let peer = transaction.getPeer(peerId) as? TelegramUser, let inputUser = apiInputUser(peer) {
return account.network.request(Api.functions.contacts.addContact(flags: 0, id: inputUser, firstName: firstName, lastName: lastName, phone: "", note: nil))
|> mapError { _ -> UpdateContactNameError in
return .generic
}
|> mapToSignal { result -> Signal<Void, UpdateContactNameError> in
account.stateManager.addUpdates(result)
return .complete()
}
} else {
return .fail(.generic)
}
}
|> mapError { _ -> UpdateContactNameError in }
|> switchToLatest
}
public enum UpdateContactNoteError {
case generic
}
func _internal_updateContactNote(account: Account, peerId: PeerId, text: String, entities: [MessageTextEntity]) -> Signal<Never, UpdateContactNoteError> {
return account.postbox.transaction { transaction -> Signal<Void, UpdateContactNoteError> in
if let peer = transaction.getPeer(peerId) as? TelegramUser, let inputUser = apiInputUser(peer) {
return account.network.request(Api.functions.contacts.updateContactNote(id: inputUser, note: .textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()))))
|> mapError { _ -> UpdateContactNoteError in
return .generic
}
|> mapToSignal { result -> Signal<Void, UpdateContactNoteError> in
return account.postbox.transaction { transaction in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { peerId, cachedData in
let cachedData = cachedData as? CachedUserData ?? CachedUserData()
return cachedData.withUpdatedNote(!text.isEmpty ? CachedUserData.Note(text: text, entities: entities) : nil)
})
}
|> castError(UpdateContactNoteError.self)
}
} else {
return .fail(.generic)
}
}
|> mapError { _ -> UpdateContactNoteError in }
|> switchToLatest
|> ignoreValues
}
@@ -0,0 +1,25 @@
import SwiftSignalKit
import Postbox
public extension TelegramEngine.EngineData.Item {
enum ChatList {
public struct FiltersDisplayTags: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = Bool
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.chatListFilters]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
let state = view.values[PreferencesKeys.chatListFilters]?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default
return state.displayTags
}
}
}
}
@@ -0,0 +1,575 @@
import SwiftSignalKit
import Postbox
public enum EngineConfiguration {
public struct Limits: Equatable {
public static let timeIntervalForever: Int32 = 0x7fffffff
public var maxGroupMemberCount: Int32
public var maxSupergroupMemberCount: Int32
public var maxMessageForwardBatchSize: Int32
public var maxRecentStickerCount: Int32
public var maxMessageEditingInterval: Int32
public var canRemoveIncomingMessagesInPrivateChats: Bool
public var maxMessageRevokeInterval: Int32
public var maxMessageRevokeIntervalInPrivateChats: Int32
public init(
maxGroupMemberCount: Int32,
maxSupergroupMemberCount: Int32,
maxMessageForwardBatchSize: Int32,
maxRecentStickerCount: Int32,
maxMessageEditingInterval: Int32,
canRemoveIncomingMessagesInPrivateChats: Bool,
maxMessageRevokeInterval: Int32,
maxMessageRevokeIntervalInPrivateChats: Int32
) {
self.maxGroupMemberCount = maxGroupMemberCount
self.maxSupergroupMemberCount = maxSupergroupMemberCount
self.maxMessageForwardBatchSize = maxMessageForwardBatchSize
self.maxRecentStickerCount = maxRecentStickerCount
self.maxMessageEditingInterval = maxMessageEditingInterval
self.canRemoveIncomingMessagesInPrivateChats = canRemoveIncomingMessagesInPrivateChats
self.maxMessageRevokeInterval = maxMessageRevokeInterval
self.maxMessageRevokeIntervalInPrivateChats = maxMessageRevokeIntervalInPrivateChats
}
}
public struct UserLimits: Equatable {
public let maxPinnedChatCount: Int32
public let maxPinnedSavedChatCount: Int32
public let maxArchivedPinnedChatCount: Int32
public let maxChannelsCount: Int32
public let maxPublicLinksCount: Int32
public let maxSavedGifCount: Int32
public let maxFavedStickerCount: Int32
public let maxFoldersCount: Int32
public let maxFolderChatsCount: Int32
public let maxCaptionLength: Int32
public let maxUploadFileParts: Int32
public let maxAboutLength: Int32
public let maxAnimatedEmojisInText: Int32
public let maxReactionsPerMessage: Int32
public let maxSharedFolderInviteLinks: Int32
public let maxSharedFolderJoin: Int32
public let maxStoryCaptionLength: Int32
public let maxExpiringStoriesCount: Int32
public let maxStoriesWeeklyCount: Int32
public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public let maxStoriesLinksCount: Int32
public let maxGiveawayChannelsCount: Int32
public let maxGiveawayCountriesCount: Int32
public let maxGiveawayPeriodSeconds: Int32
public let maxChannelRecommendationsCount: Int32
public let maxConferenceParticipantCount: Int32
public static var defaultValue: UserLimits {
return UserLimits(UserLimitsConfiguration.defaultValue)
}
public init(
maxPinnedChatCount: Int32,
maxPinnedSavedChatCount: Int32,
maxArchivedPinnedChatCount: Int32,
maxChannelsCount: Int32,
maxPublicLinksCount: Int32,
maxSavedGifCount: Int32,
maxFavedStickerCount: Int32,
maxFoldersCount: Int32,
maxFolderChatsCount: Int32,
maxCaptionLength: Int32,
maxUploadFileParts: Int32,
maxAboutLength: Int32,
maxAnimatedEmojisInText: Int32,
maxReactionsPerMessage: Int32,
maxSharedFolderInviteLinks: Int32,
maxSharedFolderJoin: Int32,
maxStoryCaptionLength: Int32,
maxExpiringStoriesCount: Int32,
maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32,
maxStoriesLinksCount: Int32,
maxGiveawayChannelsCount: Int32,
maxGiveawayCountriesCount: Int32,
maxGiveawayPeriodSeconds: Int32,
maxChannelRecommendationsCount: Int32,
maxConferenceParticipantCount: Int32
) {
self.maxPinnedChatCount = maxPinnedChatCount
self.maxPinnedSavedChatCount = maxPinnedSavedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
self.maxChannelsCount = maxChannelsCount
self.maxPublicLinksCount = maxPublicLinksCount
self.maxSavedGifCount = maxSavedGifCount
self.maxFavedStickerCount = maxFavedStickerCount
self.maxFoldersCount = maxFoldersCount
self.maxFolderChatsCount = maxFolderChatsCount
self.maxCaptionLength = maxCaptionLength
self.maxUploadFileParts = maxUploadFileParts
self.maxAboutLength = maxAboutLength
self.maxAnimatedEmojisInText = maxAnimatedEmojisInText
self.maxReactionsPerMessage = maxReactionsPerMessage
self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks
self.maxSharedFolderJoin = maxSharedFolderJoin
self.maxStoryCaptionLength = maxStoryCaptionLength
self.maxExpiringStoriesCount = maxExpiringStoriesCount
self.maxStoriesWeeklyCount = maxStoriesWeeklyCount
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
self.maxStoriesLinksCount = maxStoriesLinksCount
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds
self.maxChannelRecommendationsCount = maxChannelRecommendationsCount
self.maxConferenceParticipantCount = maxConferenceParticipantCount
}
}
}
public typealias EngineContentSettings = ContentSettings
public extension EngineConfiguration.Limits {
init(_ limitsConfiguration: LimitsConfiguration) {
self.init(
maxGroupMemberCount: limitsConfiguration.maxGroupMemberCount,
maxSupergroupMemberCount: limitsConfiguration.maxSupergroupMemberCount,
maxMessageForwardBatchSize: limitsConfiguration.maxMessageForwardBatchSize,
maxRecentStickerCount: limitsConfiguration.maxRecentStickerCount,
maxMessageEditingInterval: limitsConfiguration.maxMessageEditingInterval,
canRemoveIncomingMessagesInPrivateChats: limitsConfiguration.canRemoveIncomingMessagesInPrivateChats,
maxMessageRevokeInterval: limitsConfiguration.maxMessageRevokeInterval,
maxMessageRevokeIntervalInPrivateChats: limitsConfiguration.maxMessageRevokeIntervalInPrivateChats
)
}
func _asLimits() -> LimitsConfiguration {
return LimitsConfiguration(
maxGroupMemberCount: self.maxGroupMemberCount,
maxSupergroupMemberCount: self.maxSupergroupMemberCount,
maxMessageForwardBatchSize: self.maxMessageForwardBatchSize,
maxRecentStickerCount: self.maxRecentStickerCount,
maxMessageEditingInterval: self.maxMessageEditingInterval,
canRemoveIncomingMessagesInPrivateChats: self.canRemoveIncomingMessagesInPrivateChats,
maxMessageRevokeInterval: self.maxMessageRevokeInterval,
maxMessageRevokeIntervalInPrivateChats: self.maxMessageRevokeIntervalInPrivateChats
)
}
}
public extension EngineConfiguration.UserLimits {
init(_ userLimitsConfiguration: UserLimitsConfiguration) {
self.init(
maxPinnedChatCount: userLimitsConfiguration.maxPinnedChatCount,
maxPinnedSavedChatCount: userLimitsConfiguration.maxPinnedSavedChatCount,
maxArchivedPinnedChatCount: userLimitsConfiguration.maxArchivedPinnedChatCount,
maxChannelsCount: userLimitsConfiguration.maxChannelsCount,
maxPublicLinksCount: userLimitsConfiguration.maxPublicLinksCount,
maxSavedGifCount: userLimitsConfiguration.maxSavedGifCount,
maxFavedStickerCount: userLimitsConfiguration.maxFavedStickerCount,
maxFoldersCount: userLimitsConfiguration.maxFoldersCount,
maxFolderChatsCount: userLimitsConfiguration.maxFolderChatsCount,
maxCaptionLength: userLimitsConfiguration.maxCaptionLength,
maxUploadFileParts: userLimitsConfiguration.maxUploadFileParts,
maxAboutLength: userLimitsConfiguration.maxAboutLength,
maxAnimatedEmojisInText: userLimitsConfiguration.maxAnimatedEmojisInText,
maxReactionsPerMessage: userLimitsConfiguration.maxReactionsPerMessage,
maxSharedFolderInviteLinks: userLimitsConfiguration.maxSharedFolderInviteLinks,
maxSharedFolderJoin: userLimitsConfiguration.maxSharedFolderJoin,
maxStoryCaptionLength: userLimitsConfiguration.maxStoryCaptionLength,
maxExpiringStoriesCount: userLimitsConfiguration.maxExpiringStoriesCount,
maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount,
maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount,
maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions,
maxStoriesLinksCount: userLimitsConfiguration.maxStoriesLinksCount,
maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount,
maxGiveawayCountriesCount: userLimitsConfiguration.maxGiveawayCountriesCount,
maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds,
maxChannelRecommendationsCount: userLimitsConfiguration.maxChannelRecommendationsCount,
maxConferenceParticipantCount: userLimitsConfiguration.maxConferenceParticipantCount
)
}
}
public extension EngineConfiguration {
struct SearchBots {
public var imageBotUsername: String?
public var gifBotUsername: String?
public var venueBotUsername: String?
public init(
imageBotUsername: String?,
gifBotUsername: String?,
venueBotUsername: String?
) {
self.imageBotUsername = imageBotUsername
self.gifBotUsername = gifBotUsername
self.venueBotUsername = venueBotUsername
}
}
}
public extension EngineConfiguration.SearchBots {
init(_ configuration: SearchBotsConfiguration) {
self.init(
imageBotUsername: configuration.imageBotUsername,
gifBotUsername: configuration.gifBotUsername,
venueBotUsername: configuration.venueBotUsername
)
}
}
public extension EngineConfiguration {
struct Links {
public var autologinToken: String?
public init(
autologinToken: String?
) {
self.autologinToken = autologinToken
}
}
}
public extension EngineConfiguration.Links {
init(_ configuration: LinksConfiguration) {
self.init(
autologinToken: configuration.autologinToken
)
}
}
public extension TelegramEngine.EngineData.Item {
enum Configuration {
public struct App: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = AppConfiguration
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.appConfiguration]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else {
return AppConfiguration.defaultValue
}
return appConfiguration
}
}
public struct Limits: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = EngineConfiguration.Limits
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.limitsConfiguration]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let limitsConfiguration = view.values[PreferencesKeys.limitsConfiguration]?.get(LimitsConfiguration.self) else {
return EngineConfiguration.Limits(LimitsConfiguration.defaultValue)
}
return EngineConfiguration.Limits(limitsConfiguration)
}
}
public struct UserLimits: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = EngineConfiguration.UserLimits
fileprivate let isPremium: Bool
public init(isPremium: Bool) {
self.isPremium = isPremium
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.appConfiguration]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else {
return EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue)
}
return EngineConfiguration.UserLimits(UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: self.isPremium))
}
}
public struct SuggestedLocalization: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = SuggestedLocalizationEntry?
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.suggestedLocalization]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let suggestedLocalization = view.values[PreferencesKeys.suggestedLocalization]?.get(SuggestedLocalizationEntry.self) else {
return nil
}
return suggestedLocalization
}
}
public struct SearchBots: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = EngineConfiguration.SearchBots
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.searchBotsConfiguration]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let value = view.values[PreferencesKeys.searchBotsConfiguration]?.get(SearchBotsConfiguration.self) else {
return EngineConfiguration.SearchBots(SearchBotsConfiguration.defaultValue)
}
return EngineConfiguration.SearchBots(value)
}
}
public struct ApplicationSpecificPreference: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = PreferencesEntry?
private let itemKey: ValueBoxKey
public init(key: ValueBoxKey) {
self.itemKey = key
}
var key: PostboxViewKey {
return .preferences(keys: Set([self.itemKey]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let value = view.values[self.itemKey] else {
return nil
}
return value
}
}
public struct ContentSettings: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = EngineContentSettings
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.appConfiguration]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else {
return EngineContentSettings(appConfiguration: AppConfiguration.defaultValue)
}
return EngineContentSettings(appConfiguration: appConfiguration)
}
}
public struct LocalizationList: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = LocalizationListState
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.localizationListState]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let localizationListState = view.values[PreferencesKeys.localizationListState]?.get(LocalizationListState.self) else {
return LocalizationListState.defaultSettings
}
return localizationListState
}
}
public struct PremiumPromo: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = PremiumPromoConfiguration
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.premiumPromo]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let premiumPromoConfiguration = view.values[PreferencesKeys.premiumPromo]?.get(PremiumPromoConfiguration.self) else {
return PremiumPromoConfiguration.defaultValue
}
return premiumPromoConfiguration
}
}
public struct GlobalAutoremoveTimeout: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = Int32?
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.globalMessageAutoremoveTimeoutSettings]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let settings = view.values[PreferencesKeys.globalMessageAutoremoveTimeoutSettings]?.get(GlobalMessageAutoremoveTimeoutSettings.self) else {
return GlobalMessageAutoremoveTimeoutSettings.default.messageAutoremoveTimeout
}
return settings.messageAutoremoveTimeout
}
}
public struct Links: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = EngineConfiguration.Links
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.linksConfiguration]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let value = view.values[PreferencesKeys.linksConfiguration]?.get(LinksConfiguration.self) else {
return EngineConfiguration.Links(LinksConfiguration.defaultValue)
}
return EngineConfiguration.Links(value)
}
}
public struct GlobalPrivacy: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = GlobalPrivacySettings
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.globalPrivacySettings]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let value = view.values[PreferencesKeys.globalPrivacySettings]?.get(GlobalPrivacySettings.self) else {
return GlobalPrivacySettings.default
}
return value
}
}
public struct StoryConfigurationState: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = Stories.ConfigurationState
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.storiesConfiguration]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let value = view.values[PreferencesKeys.storiesConfiguration]?.get(Stories.ConfigurationState.self) else {
return Stories.ConfigurationState.default
}
return value
}
}
public struct AudioTranscriptionTrial: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = AudioTranscription.TrialState
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.audioTranscriptionTrialState]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
guard let value = view.values[PreferencesKeys.audioTranscriptionTrialState]?.get(AudioTranscription.TrialState.self) else {
return AudioTranscription.TrialState.defaultValue
}
return value
}
}
public struct AvailableColorOptions: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = EngineAvailableColorOptions
public let scope: PeerColorsScope
public init(scope: PeerColorsScope) {
self.scope = scope
}
var key: PostboxViewKey {
let key = ValueBoxKey(length: 8)
switch scope {
case .replies:
key.setInt64(0, value: 0)
case .profile:
key.setInt64(0, value: 1)
}
let viewKey: PostboxViewKey = .cachedItem(ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.peerColorOptions, key: key))
return viewKey
}
func extract(view: PostboxView) -> Result {
guard let view = view as? CachedItemView else {
preconditionFailure()
}
guard let value = view.value?.get(EngineAvailableColorOptions.self) else {
return EngineAvailableColorOptions(hash: 0, options: [])
}
return value
}
}
}
}
@@ -0,0 +1,78 @@
import SwiftSignalKit
import Postbox
public final class EngineContactList {
public let peers: [EnginePeer]
public let presences: [EnginePeer.Id: EnginePeer.Presence]
public init(peers: [EnginePeer], presences: [EnginePeer.Id: EnginePeer.Presence]) {
self.peers = peers
self.presences = presences
}
}
public extension TelegramEngine.EngineData.Item {
enum Contacts {
public struct List: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = EngineContactList
private let includePresences: Bool
public init(includePresences: Bool) {
self.includePresences = includePresences
}
var key: PostboxViewKey {
return .contacts(accountPeerId: nil, includePresences: self.includePresences)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? ContactPeersView else {
preconditionFailure()
}
return EngineContactList(peers: view.peers.map(EnginePeer.init), presences: view.peerPresences.mapValues(EnginePeer.Presence.init))
}
}
public struct Top: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = Array<EnginePeer.Id>
public init() {
}
var key: PostboxViewKey {
return .cachedItem(cachedRecentPeersEntryId())
}
func extract(view: PostboxView) -> [EnginePeer.Id] {
if let value = (view as? CachedItemView)?.value?.get(CachedRecentPeers.self) {
if value.enabled {
return value.ids
} else {
return []
}
} else {
return []
}
}
}
public struct CloseFriends: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = Array<EnginePeer>
public init() {
}
var key: PostboxViewKey {
return .contacts(accountPeerId: nil, includePresences: false)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? ContactPeersView else {
preconditionFailure()
}
return view.peers.filter { $0.isCloseFriend }.map(EnginePeer.init)
}
}
}
}
@@ -0,0 +1,29 @@
import SwiftSignalKit
import Postbox
public extension TelegramEngine.EngineData.Item {
enum ItemCache {
public struct Item: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = CodableEntry?
private let collectionId: Int8
private let id: ValueBoxKey
public init(collectionId: Int8, id: ValueBoxKey) {
self.collectionId = collectionId
self.id = id
}
var key: PostboxViewKey {
return .cachedItem(ItemCacheEntryId(collectionId: collectionId, key: self.id))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? CachedItemView else {
preconditionFailure()
}
return view.value
}
}
}
}
@@ -0,0 +1,500 @@
import SwiftSignalKit
import Postbox
public final class EngineTotalReadCounters {
fileprivate let state: ChatListTotalUnreadState
public init(state: ChatListTotalUnreadState) {
self.state = state
}
public func count(for category: ChatListTotalUnreadStateCategory, in statsType: ChatListTotalUnreadStateStats, with tags: PeerSummaryCounterTags) -> Int32 {
return self.state.count(for: category, in: statsType, with: tags)
}
}
public extension EngineTotalReadCounters {
func _asCounters() -> ChatListTotalUnreadState {
return self.state
}
}
public struct EnginePeerReadCounters: Equatable {
fileprivate var state: CombinedPeerReadState?
public var isMuted: Bool
public init(state: CombinedPeerReadState?, isMuted: Bool) {
self.state = state
self.isMuted = isMuted
}
public init(state: ChatListViewReadState?) {
self.state = state?.state
self.isMuted = state?.isMuted ?? false
}
public init() {
self.state = CombinedPeerReadState(states: [])
self.isMuted = false
}
public var count: Int32 {
guard let state = self.state else {
return 0
}
return state.count
}
public var markedUnread: Bool {
guard let state = self.state else {
return false
}
return state.markedUnread
}
public var isUnread: Bool {
guard let state = self.state else {
return false
}
return state.isUnread
}
public var hasEverRead: Bool {
guard let state = self.state else {
return false
}
for (_, state) in state.states {
switch state {
case let .idBased(maxIncomingReadId, _, _, _, _):
if maxIncomingReadId != 0 {
return true
}
case .indexBased:
return true
}
}
return false
}
public func isOutgoingMessageIndexRead(_ index: EngineMessage.Index) -> Bool {
guard let state = self.state else {
return false
}
return state.isOutgoingMessageIndexRead(index)
}
public func isIncomingMessageIndexRead(_ index: EngineMessage.Index) -> Bool {
guard let state = self.state else {
return false
}
return state.isIncomingMessageIndexRead(index)
}
}
public extension EnginePeerReadCounters {
init(incomingReadId: EngineMessage.Id.Id, outgoingReadId: EngineMessage.Id.Id, count: Int32, markedUnread: Bool) {
self.init(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: incomingReadId, maxOutgoingReadId: outgoingReadId, maxKnownId: max(incomingReadId, outgoingReadId), count: count, markedUnread: markedUnread))]), isMuted: false)
}
func _asReadCounters() -> CombinedPeerReadState? {
return self.state
}
}
public extension TelegramEngine.EngineData.Item {
enum Messages {
public struct Message: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Optional<EngineMessage>
fileprivate var id: EngineMessage.Id
public var mapKey: EngineMessage.Id {
return self.id
}
public init(id: EngineMessage.Id) {
self.id = id
}
var key: PostboxViewKey {
return .messages(Set([self.id]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? MessagesView else {
preconditionFailure()
}
guard let message = view.messages[self.id] else {
return nil
}
return EngineMessage(message)
}
}
public struct MessageGroup: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = [EngineMessage]
fileprivate var id: EngineMessage.Id
public init(id: EngineMessage.Id) {
self.id = id
}
var key: PostboxViewKey {
return .messageGroup(id: self.id)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? MessageGroupView else {
preconditionFailure()
}
return view.messages.map(EngineMessage.init)
}
}
public struct Messages: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = [EngineMessage.Id: EngineMessage]
fileprivate var ids: Set<EngineMessage.Id>
public init(ids: Set<EngineMessage.Id>) {
self.ids = ids
}
var key: PostboxViewKey {
return .messages(self.ids)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? MessagesView else {
preconditionFailure()
}
var result: [EngineMessage.Id: EngineMessage] = [:]
for (id, message) in view.messages {
result[id] = EngineMessage(message)
}
return result
}
}
public struct PeerReadCounters: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = EnginePeerReadCounters
fileprivate let id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
var key: PostboxViewKey {
return .combinedReadState(peerId: self.id, handleThreads: true)
}
public init(id: EnginePeer.Id) {
self.id = id
}
func extract(view: PostboxView) -> Result {
guard let view = view as? CombinedReadStateView else {
preconditionFailure()
}
return EnginePeerReadCounters(state: view.state, isMuted: false)
}
}
public struct PeerUnreadCount: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Int
fileprivate let id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
var key: PostboxViewKey {
return .unreadCounts(items: [.peer(id: self.id, handleThreads: true)])
}
public init(id: EnginePeer.Id) {
self.id = id
}
func extract(view: PostboxView) -> Result {
guard let view = view as? UnreadMessageCountsView else {
preconditionFailure()
}
return Int(view.count(for: .peer(id: self.id, handleThreads: true)) ?? 0)
}
}
public struct PeerUnreadState: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public struct Result: Equatable {
public var count: Int
public var isMarkedUnread: Bool
public init(count: Int, isMarkedUnread: Bool) {
self.count = count
self.isMarkedUnread = isMarkedUnread
}
}
fileprivate let id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
var key: PostboxViewKey {
return .unreadCounts(items: [.peer(id: self.id, handleThreads: true)])
}
public init(id: EnginePeer.Id) {
self.id = id
}
func extract(view: PostboxView) -> Result {
guard let view = view as? UnreadMessageCountsView else {
preconditionFailure()
}
if let (value, isUnread) = view.countOrUnread(for: .peer(id: self.id, handleThreads: true)) {
return Result(count: Int(value), isMarkedUnread: isUnread)
}
return Result(count: 0, isMarkedUnread: false)
}
}
public struct TotalReadCounters: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = EngineTotalReadCounters
public init() {
}
var key: PostboxViewKey {
return .unreadCounts(items: [.total(nil)])
}
func extract(view: PostboxView) -> Result {
guard let view = view as? UnreadMessageCountsView else {
preconditionFailure()
}
guard let (_, total) = view.total() else {
return EngineTotalReadCounters(state: ChatListTotalUnreadState(absoluteCounters: [:], filteredCounters: [:]))
}
return EngineTotalReadCounters(state: total)
}
}
public struct ChatListIndex: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = EngineChatList.Item.Index?
fileprivate var id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
public init(id: EnginePeer.Id) {
self.id = id
}
var key: PostboxViewKey {
return .chatListIndex(id: self.id)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? ChatListIndexView else {
preconditionFailure()
}
return view.chatListIndex.flatMap(EngineChatList.Item.Index.chatList)
}
}
public struct ChatListGroup: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = EngineChatList.Group?
fileprivate var id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
public init(id: EnginePeer.Id) {
self.id = id
}
var key: PostboxViewKey {
return .chatListIndex(id: self.id)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? ChatListIndexView else {
preconditionFailure()
}
return view.inclusion.groupId.flatMap(EngineChatList.Group.init)
}
}
public struct MessageCount: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public struct ItemKey: Hashable {
public var peerId: EnginePeer.Id
public var tag: MessageTags
public var threadId: Int64?
}
public typealias Result = Int?
fileprivate var peerId: EnginePeer.Id
fileprivate var tag: MessageTags
fileprivate var threadId: Int64?
public var mapKey: ItemKey {
return ItemKey(peerId: self.peerId, tag: self.tag, threadId: self.threadId)
}
public init(peerId: EnginePeer.Id, threadId: Int64?, tag: MessageTags) {
self.peerId = peerId
self.threadId = threadId
self.tag = tag
}
var key: PostboxViewKey {
return .historyTagSummaryView(tag: self.tag, peerId: self.peerId, threadId: self.threadId, namespace: Namespaces.Message.Cloud, customTag: nil)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? MessageHistoryTagSummaryView else {
preconditionFailure()
}
return view.count.flatMap(Int.init)
}
}
public struct ReactionTagMessageCount: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public struct ItemKey: Hashable {
public var peerId: EnginePeer.Id
public var threadId: Int64?
public var reaction: MessageReaction.Reaction
}
public typealias Result = Int?
fileprivate var peerId: EnginePeer.Id
fileprivate var threadId: Int64?
fileprivate var reaction: MessageReaction.Reaction
public var mapKey: ItemKey {
return ItemKey(peerId: self.peerId, threadId: self.threadId, reaction: self.reaction)
}
public init(peerId: EnginePeer.Id, threadId: Int64?, reaction: MessageReaction.Reaction) {
self.peerId = peerId
self.threadId = threadId
self.reaction = reaction
}
var key: PostboxViewKey {
return .historyTagSummaryView(tag: [], peerId: self.peerId, threadId: self.threadId, namespace: Namespaces.Message.Cloud, customTag: ReactionsMessageAttribute.messageTag(reaction: self.reaction))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? MessageHistoryTagSummaryView else {
preconditionFailure()
}
return view.count.flatMap(Int.init)
}
}
public struct TopMessage: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = EngineMessage?
fileprivate var id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
public init(id: EnginePeer.Id) {
self.id = id
}
var key: PostboxViewKey {
return .topChatMessage(peerIds: [self.id])
}
func extract(view: PostboxView) -> Result {
guard let view = view as? TopChatMessageView else {
preconditionFailure()
}
guard let message = view.messages[self.id] else {
return nil
}
return EngineMessage(message)
}
}
public struct SavedMessageTagStats: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = [MessageReaction.Reaction: Int]
fileprivate var peerId: EnginePeer.Id
fileprivate var threadId: Int64?
public init(peerId: EnginePeer.Id, threadId: Int64?) {
self.peerId = peerId
self.threadId = threadId
}
var key: PostboxViewKey {
return .historyCustomTagSummariesView(peerId: self.peerId, threadId: self.threadId, namespace: Namespaces.Message.Cloud)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? MessageHistoryCustomTagSummariesView else {
preconditionFailure()
}
var result: [MessageReaction.Reaction: Int] = [:]
for (key, value) in view.tags {
if let reaction = ReactionsMessageAttribute.reactionFromMessageTag(tag: key) {
result[reaction] = value
}
}
return result
}
}
public struct ThreadInfo: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = MessageHistoryThreadData?
fileprivate var peerId: EnginePeer.Id
fileprivate var threadId: Int64
public init(peerId: EnginePeer.Id, threadId: Int64) {
self.peerId = peerId
self.threadId = threadId
}
var key: PostboxViewKey {
return .messageHistoryThreadInfo(peerId: self.peerId, threadId: self.threadId)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? MessageHistoryThreadInfoView else {
preconditionFailure()
}
return view.info?.data.get(MessageHistoryThreadData.self)
}
}
public struct GlobalPostSearchState: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = TelegramGlobalPostSearchState?
public init() {
}
var key: PostboxViewKey {
return .preferences(keys: Set([PreferencesKeys.globalPostSearchState()]))
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PreferencesView else {
preconditionFailure()
}
return view.values[PreferencesKeys.globalPostSearchState()]?.get(TelegramGlobalPostSearchState.self)
}
}
}
}
@@ -0,0 +1,27 @@
import SwiftSignalKit
import Postbox
public extension TelegramEngine.EngineData.Item {
enum Notices {
public struct Notice: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = CodableEntry?
private let entryKey: NoticeEntryKey
public init(key: NoticeEntryKey) {
self.entryKey = key
}
var key: PostboxViewKey {
return .notice(key: self.entryKey)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? LocalNoticeEntryView else {
preconditionFailure()
}
return view.value
}
}
}
}
@@ -0,0 +1,69 @@
import SwiftSignalKit
import Postbox
public extension TelegramEngine.EngineData.Item {
enum Collections {
public struct FeaturedStickerPacks: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = [FeaturedStickerPackItem]
public init() {
}
var key: PostboxViewKey {
return .orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? OrderedItemListView else {
preconditionFailure()
}
return view.items.compactMap { item in
return item.contents.get(FeaturedStickerPackItem.self)
}
}
}
public struct FeaturedEmojiPacks: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = [FeaturedStickerPackItem]
public init() {
}
var key: PostboxViewKey {
return .orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? OrderedItemListView else {
preconditionFailure()
}
return view.items.compactMap { item in
return item.contents.get(FeaturedStickerPackItem.self)
}
}
}
}
enum OrderedLists {
public struct ListItems: TelegramEngineDataItem, PostboxViewDataItem {
public typealias Result = [OrderedItemListEntry]
private let collectionId: Int32
public init(collectionId: Int32) {
self.collectionId = collectionId
}
var key: PostboxViewKey {
return .orderedItemList(id: self.collectionId)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? OrderedItemListView else {
preconditionFailure()
}
return view.items
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,748 @@
import SwiftSignalKit
import Postbox
public protocol TelegramEngineDataItem {
associatedtype Result
}
public protocol TelegramEngineMapKeyDataItem {
associatedtype Key: Hashable
var mapKey: Key { get }
}
protocol AnyPostboxViewDataItem {
func keys(data: TelegramEngine.EngineData) -> [PostboxViewKey]
func _extract(data: TelegramEngine.EngineData, views: [PostboxViewKey: PostboxView]) -> Any
}
protocol PostboxViewDataItem: TelegramEngineDataItem, AnyPostboxViewDataItem {
var key: PostboxViewKey { get }
func extract(view: PostboxView) -> Result
}
extension PostboxViewDataItem {
func keys(data: TelegramEngine.EngineData) -> [PostboxViewKey] {
return [self.key]
}
func _extract(data: TelegramEngine.EngineData, views: [PostboxViewKey: PostboxView]) -> Any {
return self.extract(view: views[self.key]!)
}
}
public final class EngineDataMap<Item: TelegramEngineDataItem & TelegramEngineMapKeyDataItem>: TelegramEngineDataItem, AnyPostboxViewDataItem {
public typealias Result = [Item.Key: Item.Result]
private let items: [Item]
public init(_ items: [Item]) {
self.items = items
}
func keys(data: TelegramEngine.EngineData) -> [PostboxViewKey] {
var keys = Set<PostboxViewKey>()
for item in self.items {
for key in (item as! AnyPostboxViewDataItem).keys(data: data) {
keys.insert(key)
}
}
return Array(keys)
}
func _extract(data: TelegramEngine.EngineData, views: [PostboxViewKey: PostboxView]) -> Any {
var result: [Item.Key: Item.Result] = [:]
for item in self.items {
let itemResult = (item as! AnyPostboxViewDataItem)._extract(data: data, views: views)
result[item.mapKey] = (itemResult as! Item.Result)
}
return result
}
}
public final class EngineDataList<Item: TelegramEngineDataItem & TelegramEngineMapKeyDataItem>: TelegramEngineDataItem, AnyPostboxViewDataItem {
public typealias Result = [Item.Result]
private let items: [Item]
public init(_ items: [Item]) {
self.items = items
}
func keys(data: TelegramEngine.EngineData) -> [PostboxViewKey] {
var keys = Set<PostboxViewKey>()
for item in self.items {
for key in (item as! AnyPostboxViewDataItem).keys(data: data) {
keys.insert(key)
}
}
return Array(keys)
}
func _extract(data: TelegramEngine.EngineData, views: [PostboxViewKey: PostboxView]) -> Any {
var result: [Item.Result] = []
for item in self.items {
let itemResult = (item as! AnyPostboxViewDataItem)._extract(data: data, views: views)
result.append(itemResult as! Item.Result)
}
return result
}
}
public final class EngineDataOptional<Item: TelegramEngineDataItem>: TelegramEngineDataItem, AnyPostboxViewDataItem {
public typealias Result = Item.Result?
private let item: Item?
public init(_ item: Item?) {
self.item = item
}
func keys(data: TelegramEngine.EngineData) -> [PostboxViewKey] {
var keys = Set<PostboxViewKey>()
if let item = self.item {
for key in (item as! AnyPostboxViewDataItem).keys(data: data) {
keys.insert(key)
}
}
return Array(keys)
}
func _extract(data: TelegramEngine.EngineData, views: [PostboxViewKey: PostboxView]) -> Any {
var result: Item.Result?
if let item = self.item {
let itemResult = (item as! AnyPostboxViewDataItem)._extract(data: data, views: views)
result = (itemResult as! Item.Result)
}
return result as Any
}
}
public extension TelegramEngine {
final class EngineData {
public struct Item {
}
let accountPeerId: PeerId
private let postbox: Postbox
public init(accountPeerId: PeerId, postbox: Postbox) {
self.accountPeerId = accountPeerId
self.postbox = postbox
}
private func _subscribe(items: [AnyPostboxViewDataItem]) -> Signal<[Any], NoError> {
var keys = Set<PostboxViewKey>()
for item in items {
for key in item.keys(data: self) {
keys.insert(key)
}
}
return self.postbox.combinedView(keys: Array(keys))
|> map { views -> [Any] in
var results: [Any] = []
for item in items {
results.append(item._extract(data: self, views: views.views))
}
return results
}
}
/*public func subscribe<each T: TelegramEngineDataItem>(_ ts: repeat each T) -> Signal<repeat each T, NoError> {
}*/
public func subscribe<T0: TelegramEngineDataItem>(_ t0: T0) -> Signal<T0.Result, NoError> {
return self._subscribe(items: [t0 as! AnyPostboxViewDataItem])
|> map { results -> T0.Result in
return results[0] as! T0.Result
}
}
public func get<T0: TelegramEngineDataItem>(_ t0: T0) -> Signal<T0.Result, NoError> {
return self.subscribe(t0)
|> take(1)
}
public func subscribe<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1
) -> Signal<
(
T0.Result,
T1.Result
),
NoError> {
return self._subscribe(items: [
t0 as! AnyPostboxViewDataItem,
t1 as! AnyPostboxViewDataItem
])
|> map { results -> (T0.Result, T1.Result) in
return (
results[0] as! T0.Result,
results[1] as! T1.Result
)
}
}
public func subscribe<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result
),
NoError> {
return self._subscribe(items: [
t0 as! AnyPostboxViewDataItem,
t1 as! AnyPostboxViewDataItem,
t2 as! AnyPostboxViewDataItem
])
|> map { results -> (T0.Result, T1.Result, T2.Result) in
return (
results[0] as! T0.Result,
results[1] as! T1.Result,
results[2] as! T2.Result
)
}
}
public func subscribe<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result
),
NoError> {
return self._subscribe(items: [
t0 as! AnyPostboxViewDataItem,
t1 as! AnyPostboxViewDataItem,
t2 as! AnyPostboxViewDataItem,
t3 as! AnyPostboxViewDataItem
])
|> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result) in
return (
results[0] as! T0.Result,
results[1] as! T1.Result,
results[2] as! T2.Result,
results[3] as! T3.Result
)
}
}
public func subscribe<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem,
T4: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3,
_ t4: T4
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result,
T4.Result
),
NoError> {
return self._subscribe(items: [
t0 as! AnyPostboxViewDataItem,
t1 as! AnyPostboxViewDataItem,
t2 as! AnyPostboxViewDataItem,
t3 as! AnyPostboxViewDataItem,
t4 as! AnyPostboxViewDataItem
])
|> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result, T4.Result) in
return (
results[0] as! T0.Result,
results[1] as! T1.Result,
results[2] as! T2.Result,
results[3] as! T3.Result,
results[4] as! T4.Result
)
}
}
public func subscribe<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem,
T4: TelegramEngineDataItem,
T5: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3,
_ t4: T4,
_ t5: T5
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result,
T4.Result,
T5.Result
),
NoError> {
return self._subscribe(items: [
t0 as! AnyPostboxViewDataItem,
t1 as! AnyPostboxViewDataItem,
t2 as! AnyPostboxViewDataItem,
t3 as! AnyPostboxViewDataItem,
t4 as! AnyPostboxViewDataItem,
t5 as! AnyPostboxViewDataItem
])
|> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result, T4.Result, T5.Result) in
return (
results[0] as! T0.Result,
results[1] as! T1.Result,
results[2] as! T2.Result,
results[3] as! T3.Result,
results[4] as! T4.Result,
results[5] as! T5.Result
)
}
}
public func subscribe<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem,
T4: TelegramEngineDataItem,
T5: TelegramEngineDataItem,
T6: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3,
_ t4: T4,
_ t5: T5,
_ t6: T6
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result,
T4.Result,
T5.Result,
T6.Result
),
NoError> {
return self._subscribe(items: [
t0 as! AnyPostboxViewDataItem,
t1 as! AnyPostboxViewDataItem,
t2 as! AnyPostboxViewDataItem,
t3 as! AnyPostboxViewDataItem,
t4 as! AnyPostboxViewDataItem,
t5 as! AnyPostboxViewDataItem,
t6 as! AnyPostboxViewDataItem
])
|> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result, T4.Result, T5.Result, T6.Result) in
return (
results[0] as! T0.Result,
results[1] as! T1.Result,
results[2] as! T2.Result,
results[3] as! T3.Result,
results[4] as! T4.Result,
results[5] as! T5.Result,
results[6] as! T6.Result
)
}
}
public func subscribe<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem,
T4: TelegramEngineDataItem,
T5: TelegramEngineDataItem,
T6: TelegramEngineDataItem,
T7: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3,
_ t4: T4,
_ t5: T5,
_ t6: T6,
_ t7: T7
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result,
T4.Result,
T5.Result,
T6.Result,
T7.Result
),
NoError> {
return self._subscribe(items: [
t0 as! AnyPostboxViewDataItem,
t1 as! AnyPostboxViewDataItem,
t2 as! AnyPostboxViewDataItem,
t3 as! AnyPostboxViewDataItem,
t4 as! AnyPostboxViewDataItem,
t5 as! AnyPostboxViewDataItem,
t6 as! AnyPostboxViewDataItem,
t7 as! AnyPostboxViewDataItem
])
|> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result, T4.Result, T5.Result, T6.Result, T7.Result) in
return (
results[0] as! T0.Result,
results[1] as! T1.Result,
results[2] as! T2.Result,
results[3] as! T3.Result,
results[4] as! T4.Result,
results[5] as! T5.Result,
results[6] as! T6.Result,
results[7] as! T7.Result
)
}
}
public func subscribe<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem,
T4: TelegramEngineDataItem,
T5: TelegramEngineDataItem,
T6: TelegramEngineDataItem,
T7: TelegramEngineDataItem,
T8: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3,
_ t4: T4,
_ t5: T5,
_ t6: T6,
_ t7: T7,
_ t8: T8
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result,
T4.Result,
T5.Result,
T6.Result,
T7.Result,
T8.Result
),
NoError> {
return self._subscribe(items: [
t0 as! AnyPostboxViewDataItem,
t1 as! AnyPostboxViewDataItem,
t2 as! AnyPostboxViewDataItem,
t3 as! AnyPostboxViewDataItem,
t4 as! AnyPostboxViewDataItem,
t5 as! AnyPostboxViewDataItem,
t6 as! AnyPostboxViewDataItem,
t7 as! AnyPostboxViewDataItem,
t8 as! AnyPostboxViewDataItem
])
|> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result, T4.Result, T5.Result, T6.Result, T7.Result, T8.Result) in
return (
results[0] as! T0.Result,
results[1] as! T1.Result,
results[2] as! T2.Result,
results[3] as! T3.Result,
results[4] as! T4.Result,
results[5] as! T5.Result,
results[6] as! T6.Result,
results[7] as! T7.Result,
results[8] as! T8.Result
)
}
}
public func subscribe<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem,
T4: TelegramEngineDataItem,
T5: TelegramEngineDataItem,
T6: TelegramEngineDataItem,
T7: TelegramEngineDataItem,
T8: TelegramEngineDataItem,
T9: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3,
_ t4: T4,
_ t5: T5,
_ t6: T6,
_ t7: T7,
_ t8: T8,
_ t9: T9
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result,
T4.Result,
T5.Result,
T6.Result,
T7.Result,
T8.Result,
T9.Result
),
NoError> {
return self._subscribe(items: [
t0 as! AnyPostboxViewDataItem,
t1 as! AnyPostboxViewDataItem,
t2 as! AnyPostboxViewDataItem,
t3 as! AnyPostboxViewDataItem,
t4 as! AnyPostboxViewDataItem,
t5 as! AnyPostboxViewDataItem,
t6 as! AnyPostboxViewDataItem,
t7 as! AnyPostboxViewDataItem,
t8 as! AnyPostboxViewDataItem,
t9 as! AnyPostboxViewDataItem
])
|> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result, T4.Result, T5.Result, T6.Result, T7.Result, T8.Result, T9.Result) in
return (
results[0] as! T0.Result,
results[1] as! T1.Result,
results[2] as! T2.Result,
results[3] as! T3.Result,
results[4] as! T4.Result,
results[5] as! T5.Result,
results[6] as! T6.Result,
results[7] as! T7.Result,
results[8] as! T8.Result,
results[9] as! T9.Result
)
}
}
public func subscribe<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem,
T4: TelegramEngineDataItem,
T5: TelegramEngineDataItem,
T6: TelegramEngineDataItem,
T7: TelegramEngineDataItem,
T8: TelegramEngineDataItem,
T9: TelegramEngineDataItem,
T10: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3,
_ t4: T4,
_ t5: T5,
_ t6: T6,
_ t7: T7,
_ t8: T8,
_ t9: T9,
_ t10: T10
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result,
T4.Result,
T5.Result,
T6.Result,
T7.Result,
T8.Result,
T9.Result,
T10.Result
),
NoError> {
return self._subscribe(items: [
t0 as! AnyPostboxViewDataItem,
t1 as! AnyPostboxViewDataItem,
t2 as! AnyPostboxViewDataItem,
t3 as! AnyPostboxViewDataItem,
t4 as! AnyPostboxViewDataItem,
t5 as! AnyPostboxViewDataItem,
t6 as! AnyPostboxViewDataItem,
t7 as! AnyPostboxViewDataItem,
t8 as! AnyPostboxViewDataItem,
t9 as! AnyPostboxViewDataItem,
t10 as! AnyPostboxViewDataItem
])
|> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result, T4.Result, T5.Result, T6.Result, T7.Result, T8.Result, T9.Result, T10.Result) in
return (
results[0] as! T0.Result,
results[1] as! T1.Result,
results[2] as! T2.Result,
results[3] as! T3.Result,
results[4] as! T4.Result,
results[5] as! T5.Result,
results[6] as! T6.Result,
results[7] as! T7.Result,
results[8] as! T8.Result,
results[9] as! T9.Result,
results[10] as! T10.Result
)
}
}
public func get<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1
) -> Signal<
(
T0.Result,
T1.Result
),
NoError> {
return self.subscribe(t0, t1) |> take(1)
}
public func get<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result
),
NoError> {
return self.subscribe(t0, t1, t2) |> take(1)
}
public func get<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result
),
NoError> {
return self.subscribe(t0, t1, t2, t3) |> take(1)
}
public func get<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem,
T4: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3,
_ t4: T4
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result,
T4.Result
),
NoError> {
return self.subscribe(t0, t1, t2, t3, t4) |> take(1)
}
public func get<
T0: TelegramEngineDataItem,
T1: TelegramEngineDataItem,
T2: TelegramEngineDataItem,
T3: TelegramEngineDataItem,
T4: TelegramEngineDataItem,
T5: TelegramEngineDataItem
>(
_ t0: T0,
_ t1: T1,
_ t2: T2,
_ t3: T3,
_ t4: T4,
_ t5: T5
) -> Signal<
(
T0.Result,
T1.Result,
T2.Result,
T3.Result,
T4.Result,
T5.Result
),
NoError> {
return self.subscribe(t0, t1, t2, t3, t4, t5) |> take(1)
}
}
}
@@ -0,0 +1,259 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
public extension TelegramEngine {
final class HistoryImport {
private let postbox: Postbox
private let network: Network
public init(postbox: Postbox, network: Network) {
self.postbox = postbox
self.network = network
}
public struct Session {
fileprivate var peerId: PeerId
fileprivate var inputPeer: Api.InputPeer
fileprivate var id: Int64
}
public enum InitImportError {
case generic
case chatAdminRequired
case invalidChatType
case userBlocked
case limitExceeded
}
public enum ParsedInfo {
case privateChat(title: String?)
case group(title: String?)
case unknown(title: String?)
}
public enum GetInfoError {
case generic
case parseError
}
public func getInfo(header: String) -> Signal<ParsedInfo, GetInfoError> {
return self.network.request(Api.functions.messages.checkHistoryImport(importHead: header))
|> mapError { _ -> GetInfoError in
return .generic
}
|> mapToSignal { result -> Signal<ParsedInfo, GetInfoError> in
switch result {
case let .historyImportParsed(flags, title):
if (flags & (1 << 0)) != 0 {
return .single(.privateChat(title: title))
} else if (flags & (1 << 1)) != 0 {
return .single(.group(title: title))
} else {
return .single(.unknown(title: title))
}
}
}
}
public func initSession(peerId: PeerId, file: TempBoxFile, mediaCount: Int32) -> Signal<Session, InitImportError> {
let postbox = self.postbox
let network = self.network
return multipartUpload(network: network, postbox: postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: true, useLargerParts: true, increaseParallelParts: true, useMultiplexedRequests: false, useCompression: true)
|> mapError { _ -> InitImportError in
return .generic
}
|> mapToSignal { result -> Signal<Session, InitImportError> in
switch result {
case let .inputFile(inputFile):
return postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> castError(InitImportError.self)
|> mapToSignal { inputPeer -> Signal<Session, InitImportError> in
guard let inputPeer = inputPeer else {
return .fail(.generic)
}
return network.request(Api.functions.messages.initHistoryImport(peer: inputPeer, file: inputFile, mediaCount: mediaCount), automaticFloodWait: false)
|> mapError { error -> InitImportError in
if error.errorDescription == "CHAT_ADMIN_REQUIRED" {
return .chatAdminRequired
} else if error.errorDescription == "IMPORT_PEER_TYPE_INVALID" {
return .invalidChatType
} else if error.errorDescription == "USER_IS_BLOCKED" {
return .userBlocked
} else if error.errorDescription == "FLOOD_WAIT" {
return .limitExceeded
} else {
return .generic
}
}
|> map { result -> Session in
switch result {
case let .historyImport(id):
return Session(peerId: peerId, inputPeer: inputPeer, id: id)
}
}
}
case .progress:
return .complete()
case .inputSecretFile:
return .fail(.generic)
}
}
}
public enum MediaType {
case photo
case file
case video
case sticker
case voice
}
public enum UploadMediaError {
case generic
case chatAdminRequired
}
public func uploadMedia(session: Session, file: TempBoxFile, disposeFileAfterDone: Bool, fileName: String, mimeType: String, type: MediaType) -> Signal<Float, UploadMediaError> {
var forceNoBigParts = true
guard let size = fileSize(file.path), size != 0 else {
return .single(1.0)
}
if size >= 30 * 1024 * 1024 {
forceNoBigParts = false
}
let postbox = self.postbox
let network = self.network
return multipartUpload(network: network, postbox: postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: forceNoBigParts, useLargerParts: true, useMultiplexedRequests: true)
|> mapError { _ -> UploadMediaError in
return .generic
}
|> mapToSignal { result -> Signal<Float, UploadMediaError> in
let inputMedia: Api.InputMedia
switch result {
case let .inputFile(inputFile):
switch type {
case .photo:
inputMedia = .inputMediaUploadedPhoto(flags: 0, file: inputFile, stickers: nil, ttlSeconds: nil)
case .file, .video, .sticker, .voice:
var attributes: [Api.DocumentAttribute] = []
attributes.append(.documentAttributeFilename(fileName: fileName))
var resolvedMimeType = mimeType
switch type {
case .video:
resolvedMimeType = "video/mp4"
case .sticker:
resolvedMimeType = "image/webp"
case .voice:
resolvedMimeType = "audio/ogg"
default:
break
}
inputMedia = .inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: resolvedMimeType, attributes: attributes, stickers: nil, videoCover: nil, videoTimestamp: nil, ttlSeconds: nil)
}
case let .progress(value):
return .single(value)
case .inputSecretFile:
return .fail(.generic)
}
return network.request(Api.functions.messages.uploadImportedMedia(peer: session.inputPeer, importId: session.id, fileName: fileName, media: inputMedia))
|> mapError { error -> UploadMediaError in
switch error.errorDescription {
case "CHAT_ADMIN_REQUIRED":
return .chatAdminRequired
default:
return .generic
}
}
|> mapToSignal { result -> Signal<Float, UploadMediaError> in
return .single(1.0)
}
|> afterDisposed {
if disposeFileAfterDone {
TempBox.shared.dispose(file)
}
}
}
}
public enum StartImportError {
case generic
}
public func startImport(session: Session) -> Signal<Never, StartImportError> {
return self.network.request(Api.functions.messages.startHistoryImport(peer: session.inputPeer, importId: session.id))
|> mapError { _ -> StartImportError in
return .generic
}
|> mapToSignal { result -> Signal<Never, StartImportError> in
if case .boolTrue = result {
return .complete()
} else {
return .fail(.generic)
}
}
}
public enum CheckPeerImportResult {
case allowed
case alert(String)
}
public enum CheckPeerImportError {
case generic
case chatAdminRequired
case invalidChatType
case userBlocked
case limitExceeded
case notMutualContact
}
public func checkPeerImport(peerId: PeerId) -> Signal<CheckPeerImportResult, CheckPeerImportError> {
let postbox = self.postbox
let network = self.network
return postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> castError(CheckPeerImportError.self)
|> mapToSignal { peer -> Signal<CheckPeerImportResult, CheckPeerImportError> in
guard let peer = peer else {
return .fail(.generic)
}
guard let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
return network.request(Api.functions.messages.checkHistoryImportPeer(peer: inputPeer))
|> mapError { error -> CheckPeerImportError in
if error.errorDescription == "CHAT_ADMIN_REQUIRED" {
return .chatAdminRequired
} else if error.errorDescription == "IMPORT_PEER_TYPE_INVALID" {
return .invalidChatType
} else if error.errorDescription == "USER_IS_BLOCKED" {
return .userBlocked
} else if error.errorDescription == "USER_NOT_MUTUAL_CONTACT" {
return .notMutualContact
} else if error.errorDescription == "FLOOD_WAIT" {
return .limitExceeded
} else {
return .generic
}
}
|> map { result -> CheckPeerImportResult in
switch result {
case let .checkedHistoryImportPeer(confirmText):
if confirmText.isEmpty {
return .allowed
} else {
return .alert(confirmText)
}
}
}
}
}
}
}
@@ -0,0 +1,81 @@
import Foundation
import SwiftSignalKit
import Postbox
public typealias EngineDataBuffer = ValueBoxKey
public extension TelegramEngine {
final class ItemCache {
private let account: Account
init(account: Account) {
self.account = account
}
public func put<T: Codable>(collectionId: Int8, id: EngineDataBuffer, item: T) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
if let entry = CodableEntry(item) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id), entry: entry)
}
}
|> ignoreValues
}
public func remove(collectionId: Int8, id: EngineDataBuffer) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
transaction.removeItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id))
}
|> ignoreValues
}
public func clear(collectionIds: [Int8]) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
for id in collectionIds {
transaction.clearItemCacheCollection(collectionId: id)
}
}
|> ignoreValues
}
}
}
public extension TelegramEngineUnauthorized {
final class ItemCache {
private let account: UnauthorizedAccount
init(account: UnauthorizedAccount) {
self.account = account
}
public func put<T: Codable>(collectionId: Int8, id: EngineDataBuffer, item: T) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
if let entry = CodableEntry(item) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id), entry: entry)
}
}
|> ignoreValues
}
public func get(collectionId: Int8, id: EngineDataBuffer) -> Signal<CodableEntry?, NoError> {
return self.account.postbox.transaction { transaction -> CodableEntry? in
return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id))
}
}
public func remove(collectionId: Int8, id: EngineDataBuffer) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
transaction.removeItemCacheEntry(id: ItemCacheEntryId(collectionId: collectionId, key: id))
}
|> ignoreValues
}
public func clear(collectionIds: [Int8]) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
for id in collectionIds {
transaction.clearItemCacheCollection(collectionId: id)
}
}
|> ignoreValues
}
}
}
@@ -0,0 +1,163 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
public struct Country: Codable, Equatable {
public static func == (lhs: Country, rhs: Country) -> Bool {
return lhs.id == rhs.id && lhs.name == rhs.name && lhs.localizedName == rhs.localizedName && lhs.countryCodes == rhs.countryCodes && lhs.hidden == rhs.hidden
}
public struct CountryCode: Codable, Equatable {
public let code: String
public let prefixes: [String]
public let patterns: [String]
public init(code: String, prefixes: [String], patterns: [String]) {
self.code = code
self.prefixes = prefixes
self.patterns = patterns
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.code = try container.decode(String.self, forKey: "c")
self.prefixes = try container.decode([String].self, forKey: "pfx")
self.patterns = try container.decode([String].self, forKey: "ptrn")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.code, forKey: "c")
try container.encode(self.prefixes, forKey: "pfx")
try container.encode(self.patterns, forKey: "ptrn")
}
}
public let id: String
public let name: String
public let localizedName: String?
public let countryCodes: [CountryCode]
public let hidden: Bool
public init(id: String, name: String, localizedName: String?, countryCodes: [CountryCode], hidden: Bool) {
self.id = id
self.name = name
self.localizedName = localizedName
self.countryCodes = countryCodes
self.hidden = hidden
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.id = try container.decode(String.self, forKey: "c")
self.name = try container.decode(String.self, forKey: "n")
self.localizedName = try container.decodeIfPresent(String.self, forKey: "ln")
self.countryCodes = try container.decode([CountryCode].self, forKey: "cc")
self.hidden = try container.decode(Bool.self, forKey: "h")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.id, forKey: "c")
try container.encode(self.name, forKey: "n")
try container.encodeIfPresent(self.localizedName, forKey: "ln")
try container.encode(self.countryCodes, forKey: "cc")
try container.encode(self.hidden, forKey: "h")
}
}
public final class CountriesList: Codable, Equatable {
public let countries: [Country]
public let hash: Int32
public init(countries: [Country], hash: Int32) {
self.countries = countries
self.hash = hash
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.countries = try container.decode([Country].self, forKey: "c")
self.hash = try container.decode(Int32.self, forKey: "h")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.countries, forKey: "c")
try container.encode(self.hash, forKey: "h")
}
public static func ==(lhs: CountriesList, rhs: CountriesList) -> Bool {
return lhs.countries == rhs.countries && lhs.hash == rhs.hash
}
}
func _internal_getCountriesList(accountManager: AccountManager<TelegramAccountManagerTypes>, network: Network, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> {
let fetch: ([Country]?, Int32?) -> Signal<[Country], NoError> = { current, hash in
return network.request(Api.functions.help.getCountriesList(langCode: langCode ?? "", hash: hash ?? 0))
|> retryRequest
|> mapToSignal { result -> Signal<[Country], NoError> in
switch result {
case let .countriesList(apiCountries, hash):
let result = apiCountries.compactMap { Country(apiCountry: $0) }
if result == current {
return .complete()
} else {
let _ = accountManager.transaction { transaction in
transaction.updateSharedData(SharedDataKeys.countriesList, { _ in
return PreferencesEntry(CountriesList(countries: result, hash: hash))
})
}.start()
return .single(result)
}
case .countriesListNotModified:
return .complete()
}
}
}
if forceUpdate {
return fetch(nil, nil)
} else {
return accountManager.sharedData(keys: [SharedDataKeys.countriesList])
|> take(1)
|> map { sharedData -> ([Country], Int32) in
if let countriesList = sharedData.entries[SharedDataKeys.countriesList]?.get(CountriesList.self) {
return (countriesList.countries, countriesList.hash)
} else {
return ([], 0)
}
} |> mapToSignal { current, hash -> Signal<[Country], NoError> in
return .single(current)
|> then(fetch(current, hash))
}
}
}
extension Country.CountryCode {
init(apiCountryCode: Api.help.CountryCode) {
switch apiCountryCode {
case let .countryCode(_, countryCode, apiPrefixes, apiPatterns):
let prefixes: [String] = apiPrefixes.flatMap { $0 } ?? []
let patterns: [String] = apiPatterns.flatMap { $0 } ?? []
self.init(code: countryCode, prefixes: prefixes, patterns: patterns)
}
}
}
extension Country {
init(apiCountry: Api.help.Country) {
switch apiCountry {
case let .country(flags, iso2, defaultName, name, countryCodes):
self.init(id: iso2, name: defaultName, localizedName: name, countryCodes: countryCodes.map { Country.CountryCode(apiCountryCode: $0) }, hidden: (flags & 1 << 0) != 0)
}
}
}
@@ -0,0 +1,26 @@
import Foundation
import Postbox
import TelegramApi
extension LocalizationInfo {
init(apiLanguage: Api.LangPackLanguage) {
switch apiLanguage {
case let .langPackLanguage(flags, name, nativeName, langCode, baseLangCode, pluralCode, stringsCount, translatedCount, translationsUrl):
self.init(languageCode: langCode, baseLanguageCode: baseLangCode, customPluralizationCode: pluralCode, title: name, localizedTitle: nativeName, isOfficial: (flags & (1 << 0)) != 0, totalStringCount: stringsCount, translatedStringCount: translatedCount, platformUrl: translationsUrl)
}
}
}
public final class SuggestedLocalizationInfo {
public let languageCode: String
public let extractedEntries: [LocalizationEntry]
public let availableLocalizations: [LocalizationInfo]
init(languageCode: String, extractedEntries: [LocalizationEntry], availableLocalizations: [LocalizationInfo]) {
self.languageCode = languageCode
self.extractedEntries = extractedEntries
self.availableLocalizations = availableLocalizations
}
}
@@ -0,0 +1,66 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
func _internal_removeSavedLocalization(transaction: Transaction, languageCode: String) {
updateLocalizationListStateInteractively(transaction: transaction, { state in
var state = state
state.availableSavedLocalizations = state.availableSavedLocalizations.filter({ $0.languageCode != languageCode })
return state
})
}
func updateLocalizationListStateInteractively(postbox: Postbox, _ f: @escaping (LocalizationListState) -> LocalizationListState) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
updateLocalizationListStateInteractively(transaction: transaction, f)
}
}
func updateLocalizationListStateInteractively(transaction: Transaction, _ f: @escaping (LocalizationListState) -> LocalizationListState) {
transaction.updatePreferencesEntry(key: PreferencesKeys.localizationListState, { current in
let previous = current?.get(LocalizationListState.self) ?? LocalizationListState.defaultSettings
var updated = f(previous)
var removeOfficialIndices: [Int] = []
var officialSet = Set<String>()
for i in 0 ..< updated.availableOfficialLocalizations.count {
if officialSet.contains(updated.availableOfficialLocalizations[i].languageCode) {
removeOfficialIndices.append(i)
} else {
officialSet.insert(updated.availableOfficialLocalizations[i].languageCode)
}
}
for i in removeOfficialIndices.reversed() {
updated.availableOfficialLocalizations.remove(at: i)
}
var removeSavedIndices: [Int] = []
var savedSet = Set<String>()
for i in 0 ..< updated.availableSavedLocalizations.count {
if savedSet.contains(updated.availableSavedLocalizations[i].languageCode) {
removeSavedIndices.append(i)
} else {
savedSet.insert(updated.availableSavedLocalizations[i].languageCode)
}
}
for i in removeSavedIndices.reversed() {
updated.availableSavedLocalizations.remove(at: i)
}
return PreferencesEntry(updated)
})
}
func _internal_synchronizedLocalizationListState(postbox: Postbox, network: Network) -> Signal<Never, NoError> {
return network.request(Api.functions.langpack.getLanguages(langPack: ""))
|> retryRequest
|> mapToSignal { languages -> Signal<Never, NoError> in
let infos: [LocalizationInfo] = languages.map(LocalizationInfo.init(apiLanguage:))
return postbox.transaction { transaction -> Void in
updateLocalizationListStateInteractively(transaction: transaction, { current in
var current = current
current.availableOfficialLocalizations = infos
return current
})
}
|> ignoreValues
}
}
@@ -0,0 +1,19 @@
import Postbox
import SwiftSignalKit
import MtProtoKit
import TelegramApi
public enum RequestLocalizationPreviewError {
case generic
}
func _internal_requestLocalizationPreview(network: Network, identifier: String) -> Signal<LocalizationInfo, RequestLocalizationPreviewError> {
return network.request(Api.functions.langpack.getLanguage(langPack: "", langCode: identifier))
|> mapError { _ -> RequestLocalizationPreviewError in
return .generic
}
|> map { language -> LocalizationInfo in
return LocalizationInfo(apiLanguage: language)
}
}
@@ -0,0 +1,164 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_currentlySuggestedLocalization(network: Network, extractKeys: [String]) -> Signal<SuggestedLocalizationInfo?, NoError> {
return network.request(Api.functions.help.getConfig())
|> retryRequest
|> mapToSignal { result -> Signal<SuggestedLocalizationInfo?, NoError> in
switch result {
case let .config(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, suggestedLangCode, _, _, _, _):
if let suggestedLangCode = suggestedLangCode {
return _internal_suggestedLocalizationInfo(network: network, languageCode: suggestedLangCode, extractKeys: extractKeys) |> map(Optional.init)
} else {
return .single(nil)
}
}
}
}
func _internal_suggestedLocalizationInfo(network: Network, languageCode: String, extractKeys: [String]) -> Signal<SuggestedLocalizationInfo, NoError> {
return combineLatest(network.request(Api.functions.langpack.getLanguages(langPack: "")), network.request(Api.functions.langpack.getStrings(langPack: "", langCode: languageCode, keys: extractKeys)))
|> retryRequest
|> map { languages, strings -> SuggestedLocalizationInfo in
var entries: [LocalizationEntry] = []
for string in strings {
switch string {
case let .langPackString(key, value):
entries.append(.string(key: key, value: value))
case let .langPackStringPluralized(_, key, zeroValue, oneValue, twoValue, fewValue, manyValue, otherValue):
entries.append(.pluralizedString(key: key, zero: zeroValue, one: oneValue, two: twoValue, few: fewValue, many: manyValue, other: otherValue))
case let .langPackStringDeleted(key):
entries.append(.string(key: key, value: ""))
}
}
let infos: [LocalizationInfo] = languages.map(LocalizationInfo.init(apiLanguage:))
return SuggestedLocalizationInfo(languageCode: languageCode, extractedEntries: entries, availableLocalizations: infos)
}
}
func _internal_availableLocalizations(postbox: Postbox, network: Network, allowCached: Bool) -> Signal<[LocalizationInfo], NoError> {
let cached: Signal<[LocalizationInfo], NoError>
if allowCached {
cached = postbox.transaction { transaction -> Signal<[LocalizationInfo], NoError> in
if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAvailableLocalizations, key: ValueBoxKey(length: 0)))?.get(CachedLocalizationInfos.self) {
return .single(entry.list)
}
return .complete()
} |> switchToLatest
} else {
cached = .complete()
}
let remote = network.request(Api.functions.langpack.getLanguages(langPack: ""))
|> retryRequest
|> mapToSignal { languages -> Signal<[LocalizationInfo], NoError> in
let infos: [LocalizationInfo] = languages.map(LocalizationInfo.init(apiLanguage:))
return postbox.transaction { transaction -> [LocalizationInfo] in
if let entry = CodableEntry(CachedLocalizationInfos(list: infos)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAvailableLocalizations, key: ValueBoxKey(length: 0)), entry: entry)
}
return infos
}
}
return cached |> then(remote)
}
public enum DownloadLocalizationError {
case generic
}
func _internal_downloadLocalization(network: Network, languageCode: String) -> Signal<Localization, DownloadLocalizationError> {
return network.request(Api.functions.langpack.getLangPack(langPack: "", langCode: languageCode))
|> mapError { _ -> DownloadLocalizationError in
return .generic
}
|> map { result -> Localization in
let version: Int32
var entries: [LocalizationEntry] = []
switch result {
case let .langPackDifference(_, _, versionValue, strings):
version = versionValue
for string in strings {
switch string {
case let .langPackString(key, value):
entries.append(.string(key: key, value: value))
case let .langPackStringPluralized(_, key, zeroValue, oneValue, twoValue, fewValue, manyValue, otherValue):
entries.append(.pluralizedString(key: key, zero: zeroValue, one: oneValue, two: twoValue, few: fewValue, many: manyValue, other: otherValue))
case let .langPackStringDeleted(key):
entries.append(.string(key: key, value: ""))
}
}
}
return Localization(version: version, entries: entries)
}
}
public enum DownloadAndApplyLocalizationError {
case generic
}
func _internal_downloadAndApplyLocalization(accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, languageCode: String) -> Signal<Void, DownloadAndApplyLocalizationError> {
return _internal_requestLocalizationPreview(network: network, identifier: languageCode)
|> mapError { _ -> DownloadAndApplyLocalizationError in
return .generic
}
|> mapToSignal { preview -> Signal<Void, DownloadAndApplyLocalizationError> in
var primaryAndSecondaryLocalizations: [Signal<Localization, DownloadLocalizationError>] = []
primaryAndSecondaryLocalizations.append(_internal_downloadLocalization(network: network, languageCode: preview.languageCode))
if let secondaryCode = preview.baseLanguageCode {
primaryAndSecondaryLocalizations.append(_internal_downloadLocalization(network: network, languageCode: secondaryCode))
}
return combineLatest(primaryAndSecondaryLocalizations)
|> mapError { _ -> DownloadAndApplyLocalizationError in
return .generic
}
|> mapToSignal { components -> Signal<Void, DownloadAndApplyLocalizationError> in
guard let primaryLocalization = components.first else {
return .fail(.generic)
}
var secondaryComponent: LocalizationComponent?
if let secondaryCode = preview.baseLanguageCode, components.count > 1 {
secondaryComponent = LocalizationComponent(languageCode: secondaryCode, localizedName: "", localization: components[1], customPluralizationCode: nil)
}
return accountManager.transaction { transaction -> Signal<Void, DownloadAndApplyLocalizationError> in
transaction.updateSharedData(SharedDataKeys.localizationSettings, { _ in
return PreferencesEntry(LocalizationSettings(primaryComponent: LocalizationComponent(languageCode: preview.languageCode, localizedName: preview.localizedTitle, localization: primaryLocalization, customPluralizationCode: preview.customPluralizationCode), secondaryComponent: secondaryComponent))
})
return postbox.transaction { transaction -> Signal<Void, DownloadAndApplyLocalizationError> in
updateLocalizationListStateInteractively(transaction: transaction, { state in
var state = state
for i in 0 ..< state.availableSavedLocalizations.count {
if state.availableSavedLocalizations[i].languageCode == preview.languageCode {
state.availableSavedLocalizations.remove(at: i)
break
}
}
state.availableSavedLocalizations.insert(preview, at: 0)
return state
})
network.context.updateApiEnvironment { current in
return current?.withUpdatedLangPackCode(preview.languageCode)
}
return network.request(Api.functions.help.test())
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
|> castError(DownloadAndApplyLocalizationError.self)
}
|> castError(DownloadAndApplyLocalizationError.self)
|> switchToLatest
}
|> castError(DownloadAndApplyLocalizationError.self)
|> switchToLatest
}
}
}
@@ -0,0 +1,19 @@
import Foundation
import Postbox
import SwiftSignalKit
func _internal_markSuggestedLocalizationAsSeenInteractively(postbox: Postbox, languageCode: String) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.suggestedLocalization, { current in
if let current = current?.get(SuggestedLocalizationEntry.self) {
if current.languageCode == languageCode, !current.isSeen {
return PreferencesEntry(SuggestedLocalizationEntry(languageCode: languageCode, isSeen: true))
}
} else {
return PreferencesEntry(SuggestedLocalizationEntry(languageCode: languageCode, isSeen: true))
}
return current
})
}
}
@@ -0,0 +1,69 @@
import SwiftSignalKit
import Postbox
public extension TelegramEngine {
final class Localization {
private let account: Account
init(account: Account) {
self.account = account
}
public func getCountriesList(accountManager: AccountManager<TelegramAccountManagerTypes>, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> {
return _internal_getCountriesList(accountManager: accountManager, network: self.account.network, langCode: langCode, forceUpdate: forceUpdate)
}
public func markSuggestedLocalizationAsSeenInteractively(languageCode: String) -> Signal<Void, NoError> {
return _internal_markSuggestedLocalizationAsSeenInteractively(postbox: self.account.postbox, languageCode: languageCode)
}
public func synchronizedLocalizationListState() -> Signal<Never, NoError> {
return _internal_synchronizedLocalizationListState(postbox: self.account.postbox, network: self.account.network)
}
public func suggestedLocalizationInfo(languageCode: String, extractKeys: [String]) -> Signal<SuggestedLocalizationInfo, NoError> {
return _internal_suggestedLocalizationInfo(network: self.account.network, languageCode: languageCode, extractKeys: extractKeys)
}
public func requestLocalizationPreview(identifier: String) -> Signal<LocalizationInfo, RequestLocalizationPreviewError> {
return _internal_requestLocalizationPreview(network: self.account.network, identifier: identifier)
}
public func downloadAndApplyLocalization(accountManager: AccountManager<TelegramAccountManagerTypes>, languageCode: String) -> Signal<Void, DownloadAndApplyLocalizationError> {
return _internal_downloadAndApplyLocalization(accountManager: accountManager, postbox: self.account.postbox, network: self.account.network, languageCode: languageCode)
}
public func removeSavedLocalization(languageCode: String) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
_internal_removeSavedLocalization(transaction: transaction, languageCode: languageCode)
}
|> ignoreValues
}
}
}
public extension TelegramEngineUnauthorized {
final class Localization {
private let account: UnauthorizedAccount
init(account: UnauthorizedAccount) {
self.account = account
}
public func getCountriesList(accountManager: AccountManager<TelegramAccountManagerTypes>, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> {
return _internal_getCountriesList(accountManager: accountManager, network: self.account.network, langCode: langCode, forceUpdate: forceUpdate)
}
public func markSuggestedLocalizationAsSeenInteractively(languageCode: String) -> Signal<Void, NoError> {
return _internal_markSuggestedLocalizationAsSeenInteractively(postbox: self.account.postbox, languageCode: languageCode)
}
public func currentlySuggestedLocalization(extractKeys: [String]) -> Signal<SuggestedLocalizationInfo?, NoError> {
return _internal_currentlySuggestedLocalization(network: self.account.network, extractKeys: extractKeys)
}
public func downloadAndApplyLocalization(accountManager: AccountManager<TelegramAccountManagerTypes>, languageCode: String) -> Signal<Void, DownloadAndApplyLocalizationError> {
return _internal_downloadAndApplyLocalization(accountManager: accountManager, postbox: self.account.postbox, network: self.account.network, languageCode: languageCode)
}
}
}
@@ -0,0 +1,708 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
private class AdMessagesHistoryContextImpl {
final class CachedMessage: Equatable, Codable {
enum CodingKeys: String, CodingKey {
case opaqueId
case messageType
case title
case text
case textEntities
case media
case contentMedia
case color
case backgroundEmojiId
case url
case buttonText
case sponsorInfo
case additionalInfo
case canReport
case minDisplayDuration
case maxDisplayDuration
}
enum MessageType: Int32, Codable {
case sponsored = 0
case recommended = 1
}
public let opaqueId: Data
public let messageType: MessageType
public let title: String
public let text: String
public let textEntities: [MessageTextEntity]
public let media: [Media]
public let contentMedia: [Media]
public let color: PeerNameColor?
public let backgroundEmojiId: Int64?
public let url: String
public let buttonText: String
public let sponsorInfo: String?
public let additionalInfo: String?
public let canReport: Bool
public let minDisplayDuration: Int32?
public let maxDisplayDuration: Int32?
public init(
opaqueId: Data,
messageType: MessageType,
title: String,
text: String,
textEntities: [MessageTextEntity],
media: [Media],
contentMedia: [Media],
color: PeerNameColor?,
backgroundEmojiId: Int64?,
url: String,
buttonText: String,
sponsorInfo: String?,
additionalInfo: String?,
canReport: Bool,
minDisplayDuration: Int32?,
maxDisplayDuration: Int32?
) {
self.opaqueId = opaqueId
self.messageType = messageType
self.title = title
self.text = text
self.textEntities = textEntities
self.media = media
self.contentMedia = contentMedia
self.color = color
self.backgroundEmojiId = backgroundEmojiId
self.url = url
self.buttonText = buttonText
self.sponsorInfo = sponsorInfo
self.additionalInfo = additionalInfo
self.canReport = canReport
self.minDisplayDuration = minDisplayDuration
self.maxDisplayDuration = maxDisplayDuration
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.opaqueId = try container.decode(Data.self, forKey: .opaqueId)
if let messageType = try container.decodeIfPresent(Int32.self, forKey: .messageType) {
self.messageType = MessageType(rawValue: messageType) ?? .sponsored
} else {
self.messageType = .sponsored
}
self.title = try container.decode(String.self, forKey: .title)
self.text = try container.decode(String.self, forKey: .text)
self.textEntities = try container.decode([MessageTextEntity].self, forKey: .textEntities)
let mediaData = try container.decode([Data].self, forKey: .media)
self.media = mediaData.compactMap { data -> Media? in
return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media
}
let contentMediaData = try container.decode([Data].self, forKey: .contentMedia)
self.contentMedia = contentMediaData.compactMap { data -> Media? in
return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media
}
self.color = try container.decodeIfPresent(Int32.self, forKey: .color).flatMap { PeerNameColor(rawValue: $0) }
self.backgroundEmojiId = try container.decodeIfPresent(Int64.self, forKey: .backgroundEmojiId)
self.url = try container.decode(String.self, forKey: .url)
self.buttonText = try container.decode(String.self, forKey: .buttonText)
self.sponsorInfo = try container.decodeIfPresent(String.self, forKey: .sponsorInfo)
self.additionalInfo = try container.decodeIfPresent(String.self, forKey: .additionalInfo)
self.canReport = try container.decodeIfPresent(Bool.self, forKey: .canReport) ?? false
self.minDisplayDuration = try container.decodeIfPresent(Int32.self, forKey: .minDisplayDuration)
self.maxDisplayDuration = try container.decodeIfPresent(Int32.self, forKey: .maxDisplayDuration)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.opaqueId, forKey: .opaqueId)
try container.encode(self.messageType.rawValue, forKey: .messageType)
try container.encode(self.title, forKey: .title)
try container.encode(self.text, forKey: .text)
try container.encode(self.textEntities, forKey: .textEntities)
let mediaData = self.media.map { media -> Data in
let encoder = PostboxEncoder()
encoder.encodeRootObject(media)
return encoder.makeData()
}
try container.encode(mediaData, forKey: .media)
let contentMediaData = self.contentMedia.map { media -> Data in
let encoder = PostboxEncoder()
encoder.encodeRootObject(media)
return encoder.makeData()
}
try container.encode(contentMediaData, forKey: .contentMedia)
try container.encodeIfPresent(self.color?.rawValue, forKey: .color)
try container.encodeIfPresent(self.backgroundEmojiId, forKey: .backgroundEmojiId)
try container.encode(self.url, forKey: .url)
try container.encode(self.buttonText, forKey: .buttonText)
try container.encodeIfPresent(self.sponsorInfo, forKey: .sponsorInfo)
try container.encodeIfPresent(self.additionalInfo, forKey: .additionalInfo)
try container.encode(self.canReport, forKey: .canReport)
try container.encodeIfPresent(self.minDisplayDuration, forKey: .minDisplayDuration)
try container.encodeIfPresent(self.maxDisplayDuration, forKey: .maxDisplayDuration)
}
public static func ==(lhs: CachedMessage, rhs: CachedMessage) -> Bool {
if lhs.opaqueId != rhs.opaqueId {
return false
}
if lhs.messageType != rhs.messageType {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textEntities != rhs.textEntities {
return false
}
if lhs.media.count != rhs.media.count {
return false
}
for i in 0 ..< lhs.media.count {
if !lhs.media[i].isEqual(to: rhs.media[i]) {
return false
}
}
if lhs.contentMedia.count != rhs.contentMedia.count {
return false
}
for i in 0 ..< lhs.contentMedia.count {
if !lhs.contentMedia[i].isEqual(to: rhs.contentMedia[i]) {
return false
}
}
if lhs.url != rhs.url {
return false
}
if lhs.buttonText != rhs.buttonText {
return false
}
if lhs.sponsorInfo != rhs.sponsorInfo {
return false
}
if lhs.additionalInfo != rhs.additionalInfo {
return false
}
if lhs.canReport != rhs.canReport {
return false
}
if lhs.minDisplayDuration != rhs.minDisplayDuration {
return false
}
if lhs.maxDisplayDuration != rhs.maxDisplayDuration {
return false
}
return true
}
func toMessage(peerId: PeerId, transaction: Transaction) -> Message? {
var attributes: [MessageAttribute] = []
let mappedMessageType: AdMessageAttribute.MessageType
switch self.messageType {
case .sponsored:
mappedMessageType = .sponsored
case .recommended:
mappedMessageType = .recommended
}
let adAttribute = AdMessageAttribute(
opaqueId: self.opaqueId,
messageType: mappedMessageType,
url: self.url,
buttonText: self.buttonText,
sponsorInfo: self.sponsorInfo,
additionalInfo: self.additionalInfo,
canReport: self.canReport,
hasContentMedia: !self.contentMedia.isEmpty,
minDisplayDuration: self.minDisplayDuration,
maxDisplayDuration: self.maxDisplayDuration
)
attributes.append(adAttribute)
if !self.textEntities.isEmpty {
let entitiesAttribute = TextEntitiesMessageAttribute(entities: self.textEntities)
attributes.append(entitiesAttribute)
}
var messagePeers = SimpleDictionary<PeerId, Peer>()
if let peer = transaction.getPeer(peerId) {
messagePeers[peer.id] = peer
}
let author: Peer = TelegramChannel(
id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)),
accessHash: nil,
title: self.title,
username: nil,
photo: [],
creationDate: 0,
version: 0,
participationStatus: .left,
info: .broadcast(TelegramChannelBroadcastInfo(flags: [])),
flags: [],
restrictionInfo: nil,
adminRights: nil,
bannedRights: nil,
defaultBannedRights: nil,
usernames: [],
storiesHidden: nil,
nameColor: self.color ?? .blue,
backgroundEmojiId: self.backgroundEmojiId,
profileColor: nil,
profileBackgroundEmojiId: nil,
emojiStatus: nil,
approximateBoostLevel: nil,
subscriptionUntilDate: nil,
verificationIconFileId: nil,
sendPaidMessageStars: nil,
linkedMonoforumId: nil
)
messagePeers[author.id] = author
let messageHash = (self.text.hashValue &+ 31 &* peerId.hashValue) &* 31 &+ author.id.hashValue
let messageStableVersion = UInt32(bitPattern: Int32(truncatingIfNeeded: messageHash))
return Message(
stableId: 0,
stableVersion: messageStableVersion,
id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: Int32.max - 1,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: author,
text: self.text,
attributes: attributes,
media: !self.contentMedia.isEmpty ? self.contentMedia : self.media,
peers: messagePeers,
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
}
}
private let queue: Queue
private let account: Account
private let peerId: EnginePeer.Id
private let messageId: EngineMessage.Id?
private let maskAsSeenDisposables = DisposableDict<Data>()
struct CachedState: Codable, PostboxCoding {
enum CodingKeys: String, CodingKey {
case timestamp
case interPostInterval
case messages
}
var timestamp: Int32
var interPostInterval: Int32?
var messages: [CachedMessage]
init(timestamp: Int32, interPostInterval: Int32?, messages: [CachedMessage]) {
self.timestamp = timestamp
self.interPostInterval = interPostInterval
self.messages = messages
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
self.interPostInterval = try container.decodeIfPresent(Int32.self, forKey: .interPostInterval)
self.messages = try container.decode([CachedMessage].self, forKey: .messages)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.timestamp, forKey: .timestamp)
try container.encodeIfPresent(self.interPostInterval, forKey: .interPostInterval)
try container.encode(self.messages, forKey: .messages)
}
init(decoder: PostboxDecoder) {
self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0)
self.interPostInterval = decoder.decodeOptionalInt32ForKey("interPostInterval")
if let messagesData = decoder.decodeOptionalDataArrayForKey("messages") {
self.messages = messagesData.compactMap { data -> CachedMessage? in
return try? AdaptedPostboxDecoder().decode(CachedMessage.self, from: data)
}
} else {
self.messages = []
}
}
func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.timestamp, forKey: "timestamp")
if let interPostInterval = self.interPostInterval {
encoder.encodeInt32(interPostInterval, forKey: "interPostInterval")
} else {
encoder.encodeNil(forKey: "interPostInterval")
}
encoder.encodeDataArray(self.messages.compactMap { message -> Data? in
return try? AdaptedPostboxEncoder().encode(message)
}, forKey: "messages")
}
public static func getCached(postbox: Postbox, peerId: PeerId) -> Signal<CachedState?, NoError> {
return postbox.transaction { transaction -> CachedState? in
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key))?.get(CachedState.self) {
return entry
} else {
return nil
}
}
}
public static func setCached(transaction: Transaction, peerId: PeerId, state: CachedState?) {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
let id = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key)
if let state = state, let entry = CodableEntry(state) {
transaction.putItemCacheEntry(id: id, entry: entry)
} else {
transaction.removeItemCacheEntry(id: id)
}
}
}
struct State: Equatable {
var interPostInterval: Int32?
var startDelay: Int32?
var betweenDelay: Int32?
var messages: [Message]
static func ==(lhs: State, rhs: State) -> Bool {
if lhs.interPostInterval != rhs.interPostInterval {
return false
}
if lhs.startDelay != rhs.startDelay {
return false
}
if lhs.betweenDelay != rhs.betweenDelay {
return false
}
if lhs.messages.count != rhs.messages.count {
return false
}
for i in 0 ..< lhs.messages.count {
if lhs.messages[i].id != rhs.messages[i].id {
return false
}
if lhs.messages[i].stableId != rhs.messages[i].stableId {
return false
}
}
return true
}
}
let state = Promise<State>()
private var stateValue: State? {
didSet {
if let stateValue = self.stateValue, stateValue != oldValue {
self.state.set(.single(stateValue))
}
}
}
private var isActivated: Bool = false
private let disposable = MetaDisposable()
init(queue: Queue, account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id?, activateManually: Bool) {
self.queue = queue
self.account = account
self.peerId = peerId
self.messageId = messageId
self.stateValue = State(interPostInterval: nil, messages: [])
if messageId == nil {
self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId)
|> mapToSignal { cachedState -> Signal<State, NoError> in
if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 {
return account.postbox.transaction { transaction -> State in
return State(interPostInterval: cachedState.interPostInterval, messages: cachedState.messages.compactMap { message -> Message? in
return message.toMessage(peerId: peerId, transaction: transaction)
})
}
} else {
return .single(State(interPostInterval: nil, messages: []))
}
})
}
if !activateManually {
self.activate()
}
}
deinit {
self.disposable.dispose()
self.maskAsSeenDisposables.dispose()
}
func activate() {
if self.isActivated {
return
}
self.isActivated = true
let peerId = self.peerId
let accountPeerId = self.account.peerId
let account = self.account
let messageId = self.messageId
let signal: Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> in
guard let inputPeer else {
return .single((nil, nil, nil, []))
}
var flags: Int32 = 0
if let _ = messageId {
flags |= (1 << 0)
}
return account.network.request(Api.functions.messages.getSponsoredMessages(flags: flags, peer: inputPeer, msgId: messageId?.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.SponsoredMessages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> in
guard let result = result else {
return .single((nil, nil, nil, []))
}
return account.postbox.transaction { transaction -> (interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]) in
switch result {
case let .sponsoredMessages(_, postsBetween, startDelay, betweenDelay, messages, chats, users):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var parsedMessages: [CachedMessage] = []
for message in messages {
switch message {
case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo, minDisplayDuration, maxDisplayDuration):
var parsedEntities: [MessageTextEntity] = []
if let entities = entities {
parsedEntities = messageTextEntitiesFromApiEntities(entities)
}
let isRecommended = (flags & (1 << 5)) != 0
let canReport = (flags & (1 << 12)) != 0
var nameColorIndex: Int32?
var backgroundEmojiId: Int64?
if let color {
switch color {
case let .peerColor(_, color, backgroundEmojiIdValue):
nameColorIndex = color
backgroundEmojiId = backgroundEmojiIdValue
default:
break
}
}
let photo = photo.flatMap { telegramMediaImageFromApiPhoto($0) }
let contentMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media
parsedMessages.append(CachedMessage(
opaqueId: randomId.makeData(),
messageType: isRecommended ? .recommended : .sponsored,
title: title,
text: message,
textEntities: parsedEntities,
media: photo.flatMap { [$0] } ?? [],
contentMedia: contentMedia.flatMap { [$0] } ?? [],
color: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) },
backgroundEmojiId: backgroundEmojiId,
url: url,
buttonText: buttonText,
sponsorInfo: sponsorInfo,
additionalInfo: additionalInfo,
canReport: canReport,
minDisplayDuration: minDisplayDuration,
maxDisplayDuration: maxDisplayDuration
))
}
}
if messageId == nil {
CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages))
}
return (postsBetween, startDelay, betweenDelay, parsedMessages.compactMap { message -> Message? in
return message.toMessage(peerId: peerId, transaction: transaction)
})
case .sponsoredMessagesEmpty:
return (nil, nil, nil, [])
}
}
}
}
self.disposable.set((signal
|> deliverOn(self.queue)).start(next: { [weak self] interPostInterval, startDelay, betweenDelay, messages in
guard let strongSelf = self else {
return
}
strongSelf.stateValue = State(interPostInterval: interPostInterval, startDelay: startDelay, betweenDelay: betweenDelay, messages: messages)
}))
}
func markAsSeen(opaqueId: Data) {
let signal: Signal<Never, NoError> = self.account.network.request(Api.functions.messages.viewSponsoredMessage(randomId: Buffer(data: opaqueId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
self.maskAsSeenDisposables.set(signal.start(), forKey: opaqueId)
}
func markAction(opaqueId: Data, media: Bool, fullscreen: Bool) {
_internal_markAdAction(account: self.account, opaqueId: opaqueId, media: media, fullscreen: fullscreen)
}
func remove(opaqueId: Data) {
if var stateValue = self.stateValue {
if let index = stateValue.messages.firstIndex(where: { $0.adAttribute?.opaqueId == opaqueId }) {
stateValue.messages.remove(at: index)
self.stateValue = stateValue
}
}
let peerId = self.peerId
let _ = (self.account.postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
let id = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key)
guard var cachedState = transaction.retrieveItemCacheEntry(id: id)?.get(CachedState.self) else {
return
}
if let index = cachedState.messages.firstIndex(where: { $0.opaqueId == opaqueId }) {
cachedState.messages.remove(at: index)
if let entry = CodableEntry(cachedState) {
transaction.putItemCacheEntry(id: id, entry: entry)
}
}
}).start()
}
}
public class AdMessagesHistoryContext {
private let queue = Queue()
private let impl: QueueLocalObject<AdMessagesHistoryContextImpl>
public let peerId: EnginePeer.Id
public let messageId: EngineMessage.Id?
public var state: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
let stateDisposable = impl.state.get().start(next: { state in
subscriber.putNext((state.interPostInterval, state.messages, state.startDelay, state.betweenDelay))
})
disposable.set(stateDisposable)
}
return disposable
}
}
public init(account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id? = nil, activateManually: Bool = false) {
self.peerId = peerId
self.messageId = messageId
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId, messageId: messageId, activateManually: activateManually)
})
}
public func markAsSeen(opaqueId: Data) {
self.impl.with { impl in
impl.markAsSeen(opaqueId: opaqueId)
}
}
public func markAction(opaqueId: Data, media: Bool, fullscreen: Bool) {
self.impl.with { impl in
impl.markAction(opaqueId: opaqueId, media: media, fullscreen: fullscreen)
}
}
public func remove(opaqueId: Data) {
self.impl.with { impl in
impl.remove(opaqueId: opaqueId)
}
}
public func activate() {
self.impl.with { impl in
impl.activate()
}
}
}
func _internal_markAdAction(account: Account, opaqueId: Data, media: Bool, fullscreen: Bool) {
var flags: Int32 = 0
if media {
flags |= (1 << 0)
}
if fullscreen {
flags |= (1 << 1)
}
let signal = account.network.request(Api.functions.messages.clickSponsoredMessage(flags: flags, randomId: Buffer(data: opaqueId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
let _ = signal.start()
}
func _internal_markAdAsSeen(account: Account, opaqueId: Data) {
let signal = account.network.request(Api.functions.messages.viewSponsoredMessage(randomId: Buffer(data: opaqueId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
let _ = signal.start()
}
@@ -0,0 +1,336 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_applyMaxReadIndexInteractively(postbox: Postbox, stateManager: AccountStateManager, index: MessageIndex) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
_internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index)
}
}
func _internal_applyMaxReadIndexInteractively(transaction: Transaction, stateManager: AccountStateManager, index: MessageIndex) {
let messageIds = transaction.applyInteractiveReadMaxIndex(index)
if let channel = transaction.getPeer(index.id.peerId) as? TelegramChannel, channel.isForumOrMonoForum {
if let combinedPeerReadState = transaction.getCombinedPeerReadState(channel.id), combinedPeerReadState.count == 0 {
for item in transaction.getMessageHistoryThreadIndex(peerId: channel.id, limit: 100) {
guard var data = transaction.getMessageHistoryThreadInfo(peerId: index.id.peerId, threadId: item.threadId)?.data.get(MessageHistoryThreadData.self) else {
continue
}
guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: index.id.peerId, threadId: item.threadId, namespaces: Set([Namespaces.Message.Cloud])) else {
continue
}
if data.incomingUnreadCount != 0 {
data.incomingUnreadCount = 0
data.isMarkedUnread = false
data.maxIncomingReadId = max(messageIndex.id.id, data.maxIncomingReadId)
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: index.id.peerId, threadId: item.threadId, info: entry)
}
}
}
}
}
if index.id.peerId.namespace == Namespaces.Peer.SecretChat {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
for id in messageIds {
if let message = transaction.getMessage(id) {
for attribute in message.attributes {
if let attribute = attribute as? AutoremoveTimeoutMessageAttribute {
if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && !message.containsSecretMedia {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
let updatedAttributes = currentMessage.attributes.map({ currentAttribute -> MessageAttribute in
if let currentAttribute = currentAttribute as? AutoremoveTimeoutMessageAttribute {
return AutoremoveTimeoutMessageAttribute(timeout: currentAttribute.timeout, countdownBeginTime: timestamp)
} else {
return currentAttribute
}
})
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
})
}
break
}
}
}
}
} else if index.id.peerId.namespace == Namespaces.Peer.CloudUser || index.id.peerId.namespace == Namespaces.Peer.CloudGroup || index.id.peerId.namespace == Namespaces.Peer.CloudChannel {
stateManager.notifyAppliedIncomingReadMessages([index.id])
}
}
func applyOutgoingReadMaxIndex(transaction: Transaction, index: MessageIndex, beginCountdownAt timestamp: Int32) {
let messageIds = transaction.applyOutgoingReadMaxIndex(index)
if index.id.peerId.namespace == Namespaces.Peer.SecretChat {
for id in messageIds {
applySecretOutgoingMessageReadActions(transaction: transaction, id: id, beginCountdownAt: timestamp)
}
}
}
func maybeReadSecretOutgoingMessage(transaction: Transaction, index: MessageIndex) {
guard index.id.peerId.namespace == Namespaces.Peer.SecretChat else {
assertionFailure()
return
}
guard index.id.namespace == Namespaces.Message.Local else {
assertionFailure()
return
}
guard let combinedState = transaction.getCombinedPeerReadState(index.id.peerId) else {
return
}
if combinedState.isOutgoingMessageIndexRead(index) {
applySecretOutgoingMessageReadActions(transaction: transaction, id: index.id, beginCountdownAt: index.timestamp)
}
}
func applySecretOutgoingMessageReadActions(transaction: Transaction, id: MessageId, beginCountdownAt timestamp: Int32) {
guard id.peerId.namespace == Namespaces.Peer.SecretChat else {
assertionFailure()
return
}
guard id.namespace == Namespaces.Message.Local else {
assertionFailure()
return
}
if let message = transaction.getMessage(id), message.flags.intersection(.IsIncomingMask).isEmpty {
if message.flags.intersection([.Unsent, .Sending, .Failed]).isEmpty {
for attribute in message.attributes {
if let attribute = attribute as? AutoremoveTimeoutMessageAttribute {
if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && !message.containsSecretMedia {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
let updatedAttributes = currentMessage.attributes.map({ currentAttribute -> MessageAttribute in
if let currentAttribute = currentAttribute as? AutoremoveTimeoutMessageAttribute {
return AutoremoveTimeoutMessageAttribute(timeout: currentAttribute.timeout, countdownBeginTime: timestamp)
} else {
return currentAttribute
}
})
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
})
}
break
}
}
}
}
}
func _internal_togglePeerUnreadMarkInteractively(postbox: Postbox, network: Network, viewTracker: AccountViewTracker, peerId: PeerId, setToValue: Bool? = nil) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
_internal_togglePeerUnreadMarkInteractively(transaction: transaction, network: network, viewTracker: viewTracker, peerId: peerId, setToValue: setToValue)
}
}
func _internal_toggleForumThreadUnreadMarkInteractively(transaction: Transaction, network: Network, viewTracker: AccountViewTracker, peerId: PeerId, threadId: Int64, setToValue: Bool?) {
guard let peer = transaction.getPeer(peerId) else {
return
}
guard peer.isForumOrMonoForum else {
return
}
guard var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) else {
return
}
guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) else {
return
}
let setToValue = setToValue ?? !(data.incomingUnreadCount != 0 || data.isMarkedUnread)
if setToValue {
data.isMarkedUnread = true
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
if peer.isForum {
} else if peer.isMonoForum {
if let inputPeer = apiInputPeer(peer), let subPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) {
let _ = network.request(Api.functions.messages.markDialogUnread(flags: 1 << 0, parentPeer: inputPeer, peer: .inputDialogPeer(peer: subPeer))).start()
}
}
} else {
if data.incomingUnreadCount != 0 || data.isMarkedUnread {
data.incomingUnreadCount = 0
data.isMarkedUnread = false
data.maxIncomingReadId = max(messageIndex.id.id, data.maxIncomingReadId)
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
if peer.isForum {
if let inputPeer = apiInputPeer(peer) {
let _ = network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: Int32(clamping: threadId), readMaxId: messageIndex.id.id)).start()
}
} else if peer.isMonoForum {
if let inputPeer = apiInputPeer(peer), let subPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) {
let _ = network.request(Api.functions.messages.readSavedHistory(parentPeer: inputPeer, peer: subPeer, maxId: messageIndex.id.id)).start()
}
}
}
}
}
func _internal_markForumThreadAsReadInteractively(transaction: Transaction, network: Network, viewTracker: AccountViewTracker, peerId: PeerId, threadId: Int64) {
guard let peer = transaction.getPeer(peerId) else {
return
}
guard peer.isForumOrMonoForum else {
return
}
guard var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) else {
return
}
guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) else {
return
}
if data.incomingUnreadCount != 0 {
data.incomingUnreadCount = 0
data.isMarkedUnread = false
data.maxIncomingReadId = max(messageIndex.id.id, data.maxIncomingReadId)
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
if peer.isForum {
if let inputPeer = apiInputPeer(peer) {
let _ = network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: Int32(clamping: threadId), readMaxId: messageIndex.id.id)).start()
}
} else if peer.isMonoForum {
if let inputPeer = apiInputPeer(peer), let subPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) {
let _ = network.request(Api.functions.messages.readSavedHistory(parentPeer: inputPeer, peer: subPeer, maxId: messageIndex.id.id)).start()
}
}
}
}
func _internal_togglePeerUnreadMarkInteractively(transaction: Transaction, network: Network, viewTracker: AccountViewTracker, peerId: PeerId, setToValue: Bool? = nil) {
guard let peer = transaction.getPeer(peerId) else {
return
}
var displayAsRegularChat: Bool = false
if let channel = peer as? TelegramChannel, channel.flags.contains(.displayForumAsTabs) {
displayAsRegularChat = true
} else if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
displayAsRegularChat = cachedData.viewForumAsMessages.knownValue ?? false
}
if peer.isForumOrMonoForum, !displayAsRegularChat {
for item in transaction.getMessageHistoryThreadIndex(peerId: peerId, limit: 20) {
guard var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: item.threadId)?.data.get(MessageHistoryThreadData.self) else {
continue
}
guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: item.threadId, namespaces: Set([Namespaces.Message.Cloud])) else {
continue
}
if data.incomingUnreadCount != 0 {
data.incomingUnreadCount = 0
data.isMarkedUnread = false
data.maxIncomingReadId = max(messageIndex.id.id, data.maxIncomingReadId)
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: item.threadId, info: entry)
}
if peer.isForum {
if let inputPeer = apiInputPeer(peer) {
let _ = network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: Int32(clamping: item.threadId), readMaxId: messageIndex.id.id)).start()
}
} else if peer.isMonoForum {
if let inputPeer = apiInputPeer(peer), let subPeer = transaction.getPeer(PeerId(item.threadId)).flatMap(apiInputPeer) {
let _ = network.request(Api.functions.messages.readSavedHistory(parentPeer: inputPeer, peer: subPeer, maxId: messageIndex.id.id)).start()
}
}
}
}
} else {
let principalNamespace: MessageId.Namespace
if peerId.namespace == Namespaces.Peer.SecretChat {
principalNamespace = Namespaces.Message.SecretIncoming
} else {
principalNamespace = Namespaces.Message.Cloud
}
var hasUnread = false
if let states = transaction.getPeerReadStates(peerId) {
for state in states {
if state.1.isUnread {
hasUnread = true
break
}
}
}
if !hasUnread && peerId.namespace == Namespaces.Peer.SecretChat {
let unseenSummary = transaction.getMessageTagSummary(peerId: peerId, threadId: nil, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, customTag: nil)
let actionSummary = transaction.getPendingMessageActionsSummary(peerId: peerId, type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud)
if (unseenSummary?.count ?? 0) - (actionSummary ?? 0) > 0 {
hasUnread = true
}
}
if hasUnread {
if setToValue == nil || !(setToValue!) {
if let index = transaction.getTopPeerMessageIndex(peerId: peerId) {
let _ = transaction.applyInteractiveReadMaxIndex(index)
} else {
transaction.applyMarkUnread(peerId: peerId, namespace: principalNamespace, value: false, interactive: true)
}
viewTracker.updateMarkAllMentionsSeen(peerId: peerId, threadId: nil)
}
} else {
if setToValue == nil || setToValue! {
transaction.applyMarkUnread(peerId: peerId, namespace: principalNamespace, value: true, interactive: true)
}
}
}
}
public func clearPeerUnseenPersonalMessagesInteractively(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
if peerId.namespace == Namespaces.Peer.SecretChat {
return
}
account.viewTracker.updateMarkAllMentionsSeen(peerId: peerId, threadId: threadId)
}
|> ignoreValues
}
public func clearPeerUnseenReactionsInteractively(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
if peerId.namespace == Namespaces.Peer.SecretChat {
return
}
account.viewTracker.updateMarkAllReactionsSeen(peerId: peerId, threadId: threadId)
}
|> ignoreValues
}
func _internal_markAllChatsAsReadInteractively(transaction: Transaction, network: Network, viewTracker: AccountViewTracker, groupId: PeerGroupId, filterPredicate: ChatListFilterPredicate?) {
for peerId in transaction.getUnreadChatListPeerIds(groupId: groupId, filterPredicate: filterPredicate, additionalFilter: nil, stopOnFirstMatch: false) {
_internal_togglePeerUnreadMarkInteractively(transaction: transaction, network: network, viewTracker: viewTracker, peerId: peerId, setToValue: false)
}
}
@@ -0,0 +1,778 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
public final class AttachMenuBots: Equatable, Codable {
public final class Bot: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case peerId
case name
case botIcons
case peerTypes
case hasSettings
case flags
}
public enum IconName: Int32, Codable {
case `default` = 0
case iOSStatic
case iOSAnimated
case iOSSettingsStatic
case macOSAnimated
case macOSSettingsStatic
case placeholder
init?(string: String) {
switch string {
case "default_static":
self = .default
case "ios_static":
self = .iOSStatic
case "ios_animated":
self = .iOSAnimated
case "ios_side_menu_static":
self = .iOSSettingsStatic
case "macos_side_menu_static":
self = .macOSSettingsStatic
case "macos_animated":
self = .macOSAnimated
case "placeholder_static":
self = .placeholder
default:
return nil
}
}
}
public struct Flags: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let hasSettings = Flags(rawValue: 1 << 0)
public static let requiresWriteAccess = Flags(rawValue: 1 << 1)
public static let showInAttachMenu = Flags(rawValue: 1 << 2)
public static let showInSettings = Flags(rawValue: 1 << 3)
public static let showInSettingsDisclaimer = Flags(rawValue: 1 << 4)
public static let notActivated = Flags(rawValue: 1 << 5)
}
public struct PeerFlags: OptionSet, Codable {
public var rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let sameBot = PeerFlags(rawValue: 1 << 0)
public static let bot = PeerFlags(rawValue: 1 << 1)
public static let user = PeerFlags(rawValue: 1 << 2)
public static let group = PeerFlags(rawValue: 1 << 3)
public static let channel = PeerFlags(rawValue: 1 << 4)
public static var all: PeerFlags {
return [.sameBot, .bot, .user, .group, .channel]
}
public static var `default`: PeerFlags {
return [.sameBot, .bot, .user]
}
}
private struct IconPair: Codable {
var name: IconName
var value: TelegramMediaFile
init(_ name: IconName, value: TelegramMediaFile) {
self.name = name
self.value = value
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.name = IconName(rawValue: try container.decode(Int32.self, forKey: "k")) ?? .default
let data = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: "v")
self.value = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: data.data)))
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.name.rawValue, forKey: "k")
try container.encode(PostboxEncoder().encodeObjectToRawData(self.value), forKey: "v")
}
}
public let peerId: PeerId
public let name: String
public let icons: [IconName: TelegramMediaFile]
public let peerTypes: PeerFlags
public let flags: Flags
public init(
peerId: PeerId,
name: String,
icons: [IconName: TelegramMediaFile],
peerTypes: PeerFlags,
flags: Flags
) {
self.peerId = peerId
self.name = name
self.icons = icons
self.peerTypes = peerTypes
self.flags = flags
}
public static func ==(lhs: Bot, rhs: Bot) -> Bool {
if lhs.peerId != rhs.peerId {
return false
}
if lhs.name != rhs.name {
return false
}
if lhs.icons != rhs.icons {
return false
}
if lhs.peerTypes != rhs.peerTypes {
return false
}
if lhs.flags != rhs.flags {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let peerIdValue = try container.decode(Int64.self, forKey: .peerId)
self.peerId = PeerId(peerIdValue)
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
let iconPairs = try container.decodeIfPresent([IconPair].self, forKey: .botIcons) ?? []
var icons: [IconName: TelegramMediaFile] = [:]
for iconPair in iconPairs {
icons[iconPair.name] = iconPair.value
}
self.icons = icons
let value = try container.decodeIfPresent(Int32.self, forKey: .peerTypes) ?? Int32(PeerFlags.default.rawValue)
self.peerTypes = PeerFlags(rawValue: UInt32(value))
if let flags = try container.decodeIfPresent(Int32.self, forKey: .flags) {
self.flags = Flags(rawValue: flags)
} else {
let hasSettings = try container.decodeIfPresent(Bool.self, forKey: .hasSettings) ?? false
self.flags = hasSettings ? [.hasSettings] : []
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.peerId.toInt64(), forKey: .peerId)
try container.encode(self.name, forKey: .name)
var iconPairs: [IconPair] = []
for (key, value) in self.icons {
iconPairs.append(IconPair(key, value: value))
}
try container.encode(iconPairs, forKey: .botIcons)
try container.encode(Int32(self.peerTypes.rawValue), forKey: .peerTypes)
try container.encode(Int32(self.flags.rawValue), forKey: .flags)
}
func withUpdatedFlags(_ flags: Flags) -> Bot {
return Bot(peerId: self.peerId, name: self.name, icons: self.icons, peerTypes: self.peerTypes, flags: flags)
}
}
private enum CodingKeys: String, CodingKey {
case hash
case bots
}
public let hash: Int64
public let bots: [Bot]
public init(
hash: Int64,
bots: [Bot]
) {
self.hash = hash
self.bots = bots
}
public static func ==(lhs: AttachMenuBots, rhs: AttachMenuBots) -> Bool {
if lhs.hash != rhs.hash {
return false
}
if lhs.bots != rhs.bots {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.hash = try container.decode(Int64.self, forKey: .hash)
self.bots = try container.decode([Bot].self, forKey: .bots)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.hash, forKey: .hash)
try container.encode(self.bots, forKey: .bots)
}
}
private func cachedAttachMenuBots(postbox: Postbox) -> Signal<AttachMenuBots?, NoError> {
return postbox.transaction { transaction -> AttachMenuBots? in
return cachedAttachMenuBots(transaction: transaction)
}
}
private func cachedAttachMenuBots(transaction: Transaction) -> AttachMenuBots? {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.attachMenuBots, key: key))?.get(AttachMenuBots.self)
if let cached = cached {
return cached
} else {
return nil
}
}
private func setCachedAttachMenuBots(transaction: Transaction, attachMenuBots: AttachMenuBots) {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
let entryId = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.attachMenuBots, key: key)
if let entry = CodableEntry(attachMenuBots) {
transaction.putItemCacheEntry(id: entryId, entry: entry)
} else {
transaction.removeItemCacheEntry(id: entryId)
}
}
private func removeCachedAttachMenuBot(postbox: Postbox, botId: PeerId) -> Signal<Void, NoError> {
return postbox.transaction { transaction in
if let bots = cachedAttachMenuBots(transaction: transaction) {
let updatedBots = bots.bots.filter { $0.peerId != botId }
setCachedAttachMenuBots(transaction: transaction, attachMenuBots: AttachMenuBots(hash: bots.hash, bots: updatedBots))
}
}
}
func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, network: Network, force: Bool = false) -> Signal<Void, NoError> {
let poll = Signal<Void, NoError> { subscriber in
let signal: Signal<Void, NoError> = cachedAttachMenuBots(postbox: postbox)
|> mapToSignal { current in
return (network.request(Api.functions.messages.getAttachMenuBots(hash: force ? 0 : (current?.hash ?? 0)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.AttachMenuBots?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
guard let result = result else {
return .complete()
}
return postbox.transaction { transaction -> Void in
switch result {
case let .attachMenuBots(hash, bots, users):
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
var resultBots: [AttachMenuBots.Bot] = []
for bot in bots {
switch bot {
case let .attachMenuBot(apiFlags, botId, name, apiPeerTypes, botIcons):
var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:]
for icon in botIcons {
switch icon {
case let .attachMenuBotIcon(_, name, icon, _):
if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon, altDocuments: []) {
icons[iconName] = icon
}
}
}
if !icons.isEmpty {
var peerTypes: AttachMenuBots.Bot.PeerFlags = []
for apiType in apiPeerTypes ?? [] {
switch apiType {
case .attachMenuPeerTypeSameBotPM:
peerTypes.insert(.sameBot)
case .attachMenuPeerTypeBotPM:
peerTypes.insert(.bot)
case .attachMenuPeerTypePM:
peerTypes.insert(.user)
case .attachMenuPeerTypeChat:
peerTypes.insert(.group)
case .attachMenuPeerTypeBroadcast:
peerTypes.insert(.channel)
}
}
var flags: AttachMenuBots.Bot.Flags = []
if (apiFlags & (1 << 0)) != 0 {
flags.insert(.notActivated)
}
if (apiFlags & (1 << 1)) != 0 {
flags.insert(.hasSettings)
}
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.requiresWriteAccess)
}
if (apiFlags & (1 << 3)) != 0 {
flags.insert(.showInAttachMenu)
}
if (apiFlags & (1 << 4)) != 0 {
flags.insert(.showInSettings)
}
if (apiFlags & (1 << 5)) != 0 {
flags.insert(.showInSettingsDisclaimer)
}
resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes, flags: flags))
}
}
}
let attachMenuBots = AttachMenuBots(hash: hash, bots: resultBots)
setCachedAttachMenuBots(transaction: transaction, attachMenuBots: attachMenuBots)
case .attachMenuBotsNotModified:
break
}
return Void()
}
})
}
return signal.start(next: { value in
subscriber.putNext(value)
}, completed: {
subscriber.putCompletion()
})
}
return (
poll
|> then(
.complete()
|> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue())
)
)
|> restart
}
public enum AddBotToAttachMenuError {
case generic
}
func _internal_addBotToAttachMenu(accountPeerId: PeerId, postbox: Postbox, network: Network, botId: PeerId, allowWrite: Bool) -> Signal<Bool, AddBotToAttachMenuError> {
return postbox.transaction { transaction -> Signal<Bool, AddBotToAttachMenuError> in
guard let peer = transaction.getPeer(botId), let inputUser = apiInputUser(peer) else {
return .complete()
}
var flags: Int32 = 0
if allowWrite {
flags |= (1 << 0)
}
return network.request(Api.functions.messages.toggleBotInAttachMenu(flags: flags, bot: inputUser, enabled: .boolTrue))
|> map { value -> Bool in
switch value {
case .boolTrue:
return true
default:
return false
}
}
|> mapError { _ -> AddBotToAttachMenuError in
return .generic
}
|> mapToSignal { value -> Signal<Bool, AddBotToAttachMenuError> in
if value {
return managedSynchronizeAttachMenuBots(accountPeerId: accountPeerId, postbox: postbox, network: network, force: true)
|> castError(AddBotToAttachMenuError.self)
|> take(1)
|> map { _ -> Bool in
return true
}
} else {
return .fail(.generic)
}
}
}
|> castError(AddBotToAttachMenuError.self)
|> switchToLatest
}
func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox, network: Network, botId: PeerId) -> Signal<Bool, NoError> {
let _ = removeCachedAttachMenuBot(postbox: postbox, botId: botId).start()
return postbox.transaction { transaction -> Signal<Bool, NoError> in
guard let peer = transaction.getPeer(botId), let inputUser = apiInputUser(peer) else {
return .complete()
}
return network.request(Api.functions.messages.toggleBotInAttachMenu(flags: 0, bot: inputUser, enabled: .boolFalse))
|> map { value -> Bool in
switch value {
case .boolTrue:
return true
default:
return false
}
}
|> `catch` { error -> Signal<Bool, NoError> in
return .single(false)
}
|> afterCompleted {
let _ = (managedSynchronizeAttachMenuBots(accountPeerId: accountPeerId, postbox: postbox, network: network, force: true)
|> take(1)).start(completed: {
let _ = removeCachedAttachMenuBot(postbox: postbox, botId: botId).start()
})
}
}
|> switchToLatest
}
func _internal_acceptAttachMenuBotDisclaimer(postbox: Postbox, botId: PeerId) -> Signal<Never, NoError> {
return postbox.transaction { transaction in
if let attachMenuBots = cachedAttachMenuBots(transaction: transaction) {
var updatedAttachMenuBots = attachMenuBots
if let index = attachMenuBots.bots.firstIndex(where: { $0.peerId == botId }) {
var updatedFlags = attachMenuBots.bots[index].flags
updatedFlags.remove(.showInSettingsDisclaimer)
let updatedBot = attachMenuBots.bots[index].withUpdatedFlags(updatedFlags)
var updatedBots = attachMenuBots.bots
updatedBots[index] = updatedBot
updatedAttachMenuBots = AttachMenuBots(hash: attachMenuBots.hash, bots: updatedBots)
}
setCachedAttachMenuBots(transaction: transaction, attachMenuBots: updatedAttachMenuBots)
}
} |> ignoreValues
}
public struct AttachMenuBot: Equatable {
public let peer: EnginePeer
public let shortName: String
public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]
public let peerTypes: AttachMenuBots.Bot.PeerFlags
public let flags: AttachMenuBots.Bot.Flags
public init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) {
self.peer = peer
self.shortName = shortName
self.icons = icons
self.peerTypes = peerTypes
self.flags = flags
}
}
func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoError> {
return postbox.transaction { transaction -> [AttachMenuBot] in
guard let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots else {
return []
}
var resultBots: [AttachMenuBot] = []
for bot in cachedBots {
if let peer = transaction.getPeer(bot.peerId) {
resultBots.append(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
}
}
return resultBots
}
}
public enum GetAttachMenuBotError {
case generic
}
func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network: Network, botId: PeerId, cached: Bool) -> Signal<AttachMenuBot, GetAttachMenuBotError> {
return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in
if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots {
if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) {
return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
}
}
guard let peer = transaction.getPeer(botId), let inputUser = apiInputUser(peer) else {
return .complete()
}
return network.request(Api.functions.messages.getAttachMenuBot(bot: inputUser))
|> mapError { _ -> GetAttachMenuBotError in
return .generic
}
|> mapToSignal { result -> Signal<AttachMenuBot, GetAttachMenuBotError> in
return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in
switch result {
case let .attachMenuBotsBot(bot, users):
var peer: Peer?
for user in users {
let telegramUser = TelegramUser(user: user)
if telegramUser.id == botId {
peer = telegramUser
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
guard let peer = peer else {
return .fail(.generic)
}
switch bot {
case let .attachMenuBot(apiFlags, _, name, apiPeerTypes, botIcons):
var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:]
for icon in botIcons {
switch icon {
case let .attachMenuBotIcon(_, name, icon, _):
if let iconName = AttachMenuBots.Bot.IconName(string: name), let icon = telegramMediaFileFromApiDocument(icon, altDocuments: []) {
icons[iconName] = icon
}
}
}
var peerTypes: AttachMenuBots.Bot.PeerFlags = []
for apiType in apiPeerTypes ?? [] {
switch apiType {
case .attachMenuPeerTypeSameBotPM:
peerTypes.insert(.sameBot)
case .attachMenuPeerTypeBotPM:
peerTypes.insert(.bot)
case .attachMenuPeerTypePM:
peerTypes.insert(.user)
case .attachMenuPeerTypeChat:
peerTypes.insert(.group)
case .attachMenuPeerTypeBroadcast:
peerTypes.insert(.channel)
}
}
var flags: AttachMenuBots.Bot.Flags = []
if (apiFlags & (1 << 1)) != 0 {
flags.insert(.hasSettings)
}
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.requiresWriteAccess)
}
if (apiFlags & (1 << 3)) != 0 {
flags.insert(.showInAttachMenu)
}
if (apiFlags & (1 << 4)) != 0 {
flags.insert(.showInSettings)
}
if (apiFlags & (1 << 5)) != 0 {
flags.insert(.showInSettingsDisclaimer)
}
return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: name, icons: icons, peerTypes: peerTypes, flags: flags))
}
}
}
|> castError(GetAttachMenuBotError.self)
|> switchToLatest
}
}
|> castError(GetAttachMenuBotError.self)
|> switchToLatest
}
public enum BotAppReference : Equatable {
case id(id: Int64, accessHash: Int64)
case shortName(peerId: PeerId, shortName: String)
}
public final class BotApp: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case id
case accessHash
case shortName
case title
case description
case photo
case document
case hash
case flags
}
public struct Flags: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let notActivated = Flags(rawValue: 1 << 0)
public static let requiresWriteAccess = Flags(rawValue: 1 << 1)
public static let hasSettings = Flags(rawValue: 1 << 2)
}
public let id: Int64
public let accessHash: Int64
public let shortName: String
public let title: String
public let description: String
public let photo: TelegramMediaImage?
public let document: TelegramMediaFile?
public let hash: Int64
public let flags: Flags
public init(
id: Int64,
accessHash: Int64,
shortName: String,
title: String,
description: String,
photo: TelegramMediaImage?,
document: TelegramMediaFile?,
hash: Int64,
flags: Flags
) {
self.id = id
self.accessHash = accessHash
self.shortName = shortName
self.title = title
self.description = description
self.photo = photo
self.document = document
self.hash = hash
self.flags = flags
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
self.accessHash = try container.decode(Int64.self, forKey: .accessHash)
self.shortName = try container.decode(String.self, forKey: .shortName)
self.title = try container.decode(String.self, forKey: .title)
self.description = try container.decode(String.self, forKey: .description)
self.photo = try container.decodeIfPresent(TelegramMediaImage.self, forKey: .photo)
self.document = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .document)
self.hash = try container.decode(Int64.self, forKey: .hash)
self.flags = Flags(rawValue: try container.decode(Int32.self, forKey: .flags))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.accessHash, forKey: .accessHash)
try container.encode(self.shortName, forKey: .shortName)
try container.encode(self.title, forKey: .title)
try container.encode(self.description, forKey: .description)
try container.encodeIfPresent(self.photo, forKey: .photo)
try container.encodeIfPresent(self.document, forKey: .document)
try container.encode(self.hash, forKey: .hash)
try container.encode(self.flags.rawValue, forKey: .flags)
}
public static func ==(lhs: BotApp, rhs: BotApp) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.accessHash != rhs.accessHash {
return false
}
if lhs.shortName != rhs.shortName {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.description != rhs.description {
return false
}
if lhs.photo != rhs.photo {
return false
}
if lhs.document != rhs.document {
return false
}
if lhs.hash != rhs.hash {
return false
}
if lhs.flags != rhs.flags {
return false
}
return true
}
}
public enum GetBotAppError {
case generic
}
func _internal_getBotApp(account: Account, reference: BotAppReference) -> Signal<BotApp, GetBotAppError> {
return account.postbox.transaction { transaction -> Signal<BotApp, GetBotAppError> in
let app: Api.InputBotApp
switch reference {
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)
}
return account.network.request(Api.functions.messages.getBotApp(app: app, hash: 0))
|> mapError { _ -> GetBotAppError in
return .generic
}
|> mapToSignal { result -> Signal<BotApp, GetBotAppError> in
switch result {
case let .botApp(botAppFlags, app):
switch app {
case let .botApp(flags, id, accessHash, shortName, title, description, photo, document, hash):
let _ = flags
var appFlags = BotApp.Flags()
if (botAppFlags & (1 << 0)) != 0 {
appFlags.insert(.notActivated)
}
if (botAppFlags & (1 << 1)) != 0 {
appFlags.insert(.requiresWriteAccess)
}
if (botAppFlags & (1 << 2)) != 0 {
appFlags.insert(.hasSettings)
}
return .single(BotApp(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, hash: hash, flags: appFlags))
case .botAppNotModified:
return .complete()
}
}
}
}
|> castError(GetBotAppError.self)
|> switchToLatest
}
extension BotApp {
convenience init?(apiBotApp: Api.BotApp) {
switch apiBotApp {
case let .botApp(_, id, accessHash, shortName, title, description, photo, document, hash):
self.init(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, hash: hash, flags: [])
case .botAppNotModified:
return nil
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
import Postbox
public final class EngineCallList {
public enum Scope {
case all
case missed
}
public enum Item {
case message(message: EngineMessage, group: [EngineMessage])
case hole(EngineMessage.Index)
}
public let items: [Item]
public let hasEarlier: Bool
public let hasLater: Bool
init(
items: [Item],
hasEarlier: Bool,
hasLater: Bool
) {
self.items = items
self.hasEarlier = hasEarlier
self.hasLater = hasLater
}
}
@@ -0,0 +1,663 @@
import Postbox
public final class EngineChatList: Equatable {
public enum Group {
case root
case archive
}
public typealias MessageTagSummaryInfo = ChatListMessageTagSummaryInfo
public typealias StoryStats = PeerStoryStats
public enum PinnedItem {
public typealias Id = PinnedItemId
}
public enum RelativePosition {
case later(than: EngineChatList.Item.Index?)
case earlier(than: EngineChatList.Item.Index?)
}
public struct Draft: Equatable {
public var text: String
public var entities: [MessageTextEntity]
public init(text: String, entities: [MessageTextEntity]) {
self.text = text
self.entities = entities
}
}
public final class ForumTopicData: Equatable {
public let id: Int64
public let title: String
public let iconFileId: Int64?
public let iconColor: Int32
public let maxOutgoingReadMessageId: EngineMessage.Id
public let isUnread: Bool
public let threadPeer: EnginePeer?
public init(id: Int64, title: String, iconFileId: Int64?, iconColor: Int32, maxOutgoingReadMessageId: EngineMessage.Id, isUnread: Bool, threadPeer: EnginePeer?) {
self.id = id
self.title = title
self.iconFileId = iconFileId
self.iconColor = iconColor
self.maxOutgoingReadMessageId = maxOutgoingReadMessageId
self.isUnread = isUnread
self.threadPeer = threadPeer
}
public static func ==(lhs: ForumTopicData, rhs: ForumTopicData) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.iconFileId != rhs.iconFileId {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
if lhs.maxOutgoingReadMessageId != rhs.maxOutgoingReadMessageId {
return false
}
if lhs.isUnread != rhs.isUnread {
return false
}
if lhs.threadPeer != rhs.threadPeer {
return false
}
return true
}
}
public enum MediaDraftContentType: Int32 {
case audio
case video
}
public final class Item: Equatable {
public enum Id: Hashable {
case chatList(EnginePeer.Id)
case forum(Int64)
}
public enum PinnedIndex: Equatable, Comparable {
case none
case index(Int)
public static func <(lhs: PinnedIndex, rhs: PinnedIndex) -> Bool {
switch lhs {
case .none:
switch rhs {
case .none:
return false
case .index:
return false
}
case let .index(lhsValue):
switch rhs {
case .none:
return true
case let .index(rhsValue):
return lhsValue >= rhsValue
}
}
}
}
public enum Index: Equatable, Comparable {
public typealias ChatList = ChatListIndex
case chatList(ChatListIndex)
case forum(pinnedIndex: PinnedIndex, timestamp: Int32, threadId: Int64, namespace: EngineMessage.Id.Namespace, id: EngineMessage.Id.Id)
public static func <(lhs: Index, rhs: Index) -> Bool {
switch lhs {
case let .chatList(lhsIndex):
if case let .chatList(rhsIndex) = rhs {
return lhsIndex < rhsIndex
} else {
return true
}
case let .forum(lhsPinnedIndex, lhsTimestamp, lhsThreadId, lhsNamespace, lhsId):
if case let .forum(rhsPinnedIndex, rhsTimestamp, rhsThreadId, rhsNamespace, rhsId) = rhs {
if lhsPinnedIndex != rhsPinnedIndex {
return lhsPinnedIndex < rhsPinnedIndex
}
if lhsTimestamp != rhsTimestamp {
return lhsTimestamp < rhsTimestamp
}
if lhsThreadId != rhsThreadId {
return lhsThreadId < rhsThreadId
}
if lhsNamespace != rhsNamespace {
return lhsNamespace < rhsNamespace
}
return lhsId < rhsId
} else {
return false
}
}
}
}
public let id: Id
public let index: Index
public let messages: [EngineMessage]
public let readCounters: EnginePeerReadCounters?
public let isMuted: Bool
public let draft: Draft?
public let threadData: MessageHistoryThreadData?
public let renderedPeer: EngineRenderedPeer
public let presence: EnginePeer.Presence?
public let hasUnseenMentions: Bool
public let hasUnseenReactions: Bool
public let forumTopicData: ForumTopicData?
public let topForumTopicItems: [EngineChatList.ForumTopicData]
public let hasFailed: Bool
public let isContact: Bool
public let autoremoveTimeout: Int32?
public let storyStats: StoryStats?
public let displayAsTopicList: Bool
public let isPremiumRequiredToMessage: Bool
public let mediaDraftContentType: EngineChatList.MediaDraftContentType?
public init(
id: Id,
index: Index,
messages: [EngineMessage],
readCounters: EnginePeerReadCounters?,
isMuted: Bool,
draft: Draft?,
threadData: MessageHistoryThreadData?,
renderedPeer: EngineRenderedPeer,
presence: EnginePeer.Presence?,
hasUnseenMentions: Bool,
hasUnseenReactions: Bool,
forumTopicData: ForumTopicData?,
topForumTopicItems: [EngineChatList.ForumTopicData],
hasFailed: Bool,
isContact: Bool,
autoremoveTimeout: Int32?,
storyStats: StoryStats?,
displayAsTopicList: Bool,
isPremiumRequiredToMessage: Bool,
mediaDraftContentType: EngineChatList.MediaDraftContentType?
) {
self.id = id
self.index = index
self.messages = messages
self.readCounters = readCounters
self.isMuted = isMuted
self.draft = draft
self.threadData = threadData
self.renderedPeer = renderedPeer
self.presence = presence
self.hasUnseenMentions = hasUnseenMentions
self.hasUnseenReactions = hasUnseenReactions
self.forumTopicData = forumTopicData
self.topForumTopicItems = topForumTopicItems
self.hasFailed = hasFailed
self.isContact = isContact
self.autoremoveTimeout = autoremoveTimeout
self.storyStats = storyStats
self.displayAsTopicList = displayAsTopicList
self.isPremiumRequiredToMessage = isPremiumRequiredToMessage
self.mediaDraftContentType = mediaDraftContentType
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.index != rhs.index {
return false
}
if lhs.messages != rhs.messages {
return false
}
if lhs.readCounters != rhs.readCounters {
return false
}
if lhs.isMuted != rhs.isMuted {
return false
}
if lhs.draft != rhs.draft {
return false
}
if lhs.threadData != rhs.threadData {
return false
}
if lhs.renderedPeer != rhs.renderedPeer {
return false
}
if lhs.presence != rhs.presence {
return false
}
if lhs.hasUnseenMentions != rhs.hasUnseenMentions {
return false
}
if lhs.hasUnseenReactions != rhs.hasUnseenReactions {
return false
}
if lhs.forumTopicData != rhs.forumTopicData {
return false
}
if lhs.topForumTopicItems != rhs.topForumTopicItems {
return false
}
if lhs.hasFailed != rhs.hasFailed {
return false
}
if lhs.isContact != rhs.isContact {
return false
}
if lhs.autoremoveTimeout != rhs.autoremoveTimeout {
return false
}
if lhs.storyStats != rhs.storyStats {
return false
}
if lhs.displayAsTopicList != rhs.displayAsTopicList {
return false
}
if lhs.isPremiumRequiredToMessage != rhs.isPremiumRequiredToMessage {
return false
}
if lhs.mediaDraftContentType != rhs.mediaDraftContentType {
return false
}
return true
}
}
public final class GroupItem: Equatable {
public final class Item: Equatable {
public let peer: EngineRenderedPeer
public let isUnread: Bool
public init(peer: EngineRenderedPeer, isUnread: Bool) {
self.peer = peer
self.isUnread = isUnread
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.isUnread != rhs.isUnread {
return false
}
return true
}
}
public let id: Group
public let topMessage: EngineMessage?
public let items: [Item]
public let unreadCount: Int
public init(
id: Group,
topMessage: EngineMessage?,
items: [Item],
unreadCount: Int
) {
self.id = id
self.topMessage = topMessage
self.items = items
self.unreadCount = unreadCount
}
public static func ==(lhs: GroupItem, rhs: GroupItem) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.topMessage?.index != rhs.topMessage?.index {
return false
}
if lhs.topMessage?.stableVersion != rhs.topMessage?.stableVersion {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.unreadCount != rhs.unreadCount {
return false
}
return true
}
}
public final class AdditionalItem: Equatable {
public final class PromoInfo: Equatable {
public enum Content: Equatable {
case proxy
case psa(type: String, message: String?)
}
public let content: Content
public init(content: Content) {
self.content = content
}
public static func ==(lhs: PromoInfo, rhs: PromoInfo) -> Bool {
if lhs.content != rhs.content {
return false
}
return true
}
}
public let item: Item
public let promoInfo: PromoInfo
public init(item: Item, promoInfo: PromoInfo) {
self.item = item
self.promoInfo = promoInfo
}
public static func ==(lhs: AdditionalItem, rhs: AdditionalItem) -> Bool {
if lhs.item != rhs.item {
return false
}
if lhs.promoInfo != rhs.promoInfo {
return false
}
return true
}
}
public let items: [Item]
public let groupItems: [GroupItem]
public let additionalItems: [AdditionalItem]
public let hasEarlier: Bool
public let hasLater: Bool
public let isLoading: Bool
public init(
items: [Item],
groupItems: [GroupItem],
additionalItems: [AdditionalItem],
hasEarlier: Bool,
hasLater: Bool,
isLoading: Bool
) {
self.items = items
self.groupItems = groupItems
self.additionalItems = additionalItems
self.hasEarlier = hasEarlier
self.hasLater = hasLater
self.isLoading = isLoading
}
public static func ==(lhs: EngineChatList, rhs: EngineChatList) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.groupItems != rhs.groupItems {
return false
}
if lhs.additionalItems != rhs.additionalItems {
return false
}
if lhs.hasEarlier != rhs.hasEarlier {
return false
}
if lhs.hasLater != rhs.hasLater {
return false
}
if lhs.isLoading != rhs.isLoading {
return false
}
return true
}
}
public extension EngineChatList.Group {
init(_ group: PeerGroupId) {
switch group {
case .root:
self = .root
case let .group(value):
assert(value == Namespaces.PeerGroup.archive.rawValue)
self = .archive
}
}
func _asGroup() -> PeerGroupId {
switch self {
case .root:
return .root
case .archive:
return Namespaces.PeerGroup.archive
}
}
}
public extension EngineChatList.RelativePosition {
init(_ position: ChatListRelativePosition) {
switch position {
case let .earlier(than):
self = .earlier(than: than.flatMap(EngineChatList.Item.Index.chatList))
case let .later(than):
self = .later(than: than.flatMap(EngineChatList.Item.Index.chatList))
}
}
func _asPosition() -> ChatListRelativePosition? {
switch self {
case let .earlier(than):
guard case let .chatList(than) = than else {
return nil
}
return .earlier(than: than)
case let .later(than):
guard case let .chatList(than) = than else {
return nil
}
return .later(than: than)
}
}
}
private func calculateIsPremiumRequiredToMessage(isPremium: Bool, targetPeer: Peer, cachedIsPremiumRequired: Bool) -> Bool {
if isPremium {
return false
}
guard let targetPeer = targetPeer as? TelegramUser else {
return false
}
if !targetPeer.flags.contains(.requirePremium) {
return false
}
return cachedIsPremiumRequired
}
extension EngineChatList.Item {
convenience init?(_ entry: ChatListEntry, isPremium: Bool, displayAsTopicList: Bool) {
switch entry {
case let .MessageEntry(entryData):
let index = entryData.index
let messages = entryData.messages
let readState = entryData.readState
let isRemovedFromTotalUnreadCount = entryData.isRemovedFromTotalUnreadCount
let embeddedState = entryData.embeddedInterfaceState
let renderedPeer = entryData.renderedPeer
let presence = entryData.presence
let tagSummaryInfo = entryData.summaryInfo
let forumTopicData = entryData.forumTopicData
let topForumTopics = entryData.topForumTopics
let hasFailed = entryData.hasFailed
let isContact = entryData.isContact
let autoremoveTimeout = entryData.autoremoveTimeout
var isPremiumRequiredToMessage = false
if let targetPeer = renderedPeer.chatMainPeer, let extractedData = entryData.extractedCachedData?.base as? ExtractedChatListItemCachedData {
isPremiumRequiredToMessage = calculateIsPremiumRequiredToMessage(isPremium: isPremium, targetPeer: targetPeer, cachedIsPremiumRequired: extractedData.isPremiumRequiredToMessage)
}
var draft: EngineChatList.Draft?
var mediaDraftContentType: EngineChatList.MediaDraftContentType?
if let embeddedState = embeddedState, let _ = embeddedState.overrideChatTimestamp {
if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) {
if let text = opaqueState.synchronizeableInputState?.text {
draft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? [])
}
mediaDraftContentType = opaqueState.mediaDraftState?.contentType
}
}
var hasUnseenMentions = false
if let info = tagSummaryInfo[ChatListEntryMessageTagSummaryKey(
tag: .unseenPersonalMessage,
actionType: PendingMessageActionType.consumeUnseenPersonalMessage
)] {
hasUnseenMentions = (info.tagSummaryCount ?? 0) > (info.actionsSummaryCount ?? 0)
}
var hasUnseenReactions = false
if let info = tagSummaryInfo[ChatListEntryMessageTagSummaryKey(
tag: .unseenReaction,
actionType: PendingMessageActionType.readReaction
)] {
hasUnseenReactions = (info.tagSummaryCount ?? 0) != 0// > (info.actionsSummaryCount ?? 0)
}
var forumTopicDataValue: EngineChatList.ForumTopicData?
if let forumTopicData {
let id = forumTopicData.id
if let forumTopicInfo = forumTopicData.info.data.get(MessageHistoryThreadData.self) {
forumTopicDataValue = EngineChatList.ForumTopicData(id: id, title: forumTopicInfo.info.title, iconFileId: forumTopicInfo.info.icon, iconColor: forumTopicInfo.info.iconColor, maxOutgoingReadMessageId: MessageId(peerId: index.messageIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: forumTopicInfo.maxOutgoingReadId), isUnread: forumTopicInfo.incomingUnreadCount > 0, threadPeer: forumTopicData.threadPeer.flatMap(EnginePeer.init))
}
}
var topForumTopicItems: [EngineChatList.ForumTopicData] = []
for item in topForumTopics {
if let forumTopicInfo = item.info.data.get(MessageHistoryThreadData.self) {
topForumTopicItems.append(EngineChatList.ForumTopicData(id: item.id, title: forumTopicInfo.info.title, iconFileId: forumTopicInfo.info.icon, iconColor: forumTopicInfo.info.iconColor, maxOutgoingReadMessageId: MessageId(peerId: index.messageIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: forumTopicInfo.maxOutgoingReadId), isUnread: forumTopicInfo.incomingUnreadCount > 0, threadPeer: item.threadPeer.flatMap(EnginePeer.init)))
}
}
let readCounters = readState.flatMap(EnginePeerReadCounters.init)
if let channel = renderedPeer.peer as? TelegramChannel {
if channel.isForumOrMonoForum {
draft = nil
} else {
forumTopicDataValue = nil
topForumTopicItems = []
}
}
self.init(
id: .chatList(index.messageIndex.id.peerId),
index: .chatList(index),
messages: messages.map(EngineMessage.init),
readCounters: readCounters,
isMuted: isRemovedFromTotalUnreadCount,
draft: draft,
threadData: nil,
renderedPeer: EngineRenderedPeer(renderedPeer),
presence: presence.flatMap(EnginePeer.Presence.init),
hasUnseenMentions: hasUnseenMentions,
hasUnseenReactions: hasUnseenReactions,
forumTopicData: forumTopicDataValue,
topForumTopicItems: topForumTopicItems,
hasFailed: hasFailed,
isContact: isContact,
autoremoveTimeout: autoremoveTimeout,
storyStats: entryData.storyStats,
displayAsTopicList: displayAsTopicList,
isPremiumRequiredToMessage: isPremiumRequiredToMessage,
mediaDraftContentType: mediaDraftContentType
)
case .HoleEntry:
return nil
}
}
}
extension EngineChatList.GroupItem {
convenience init(_ entry: ChatListGroupReferenceEntry) {
self.init(
id: EngineChatList.Group(entry.groupId),
topMessage: entry.message.flatMap(EngineMessage.init),
items: entry.renderedPeers.map { peer in
return EngineChatList.GroupItem.Item(
peer: EngineRenderedPeer(peer.peer),
isUnread: peer.isUnread
)
},
unreadCount: Int(entry.unreadState.count(countingCategory: .chats, mutedCategory: .all))
)
}
}
extension EngineChatList.AdditionalItem.PromoInfo {
convenience init(_ item: PromoChatListItem) {
let content: EngineChatList.AdditionalItem.PromoInfo.Content
switch item.kind {
case .proxy:
content = .proxy
case let .psa(type, message):
content = .psa(type: type, message: message)
}
self.init(content: content)
}
}
extension EngineChatList.AdditionalItem {
convenience init?(_ entry: ChatListAdditionalItemEntry) {
guard let item = EngineChatList.Item(entry.entry, isPremium: false, displayAsTopicList: false) else {
return nil
}
guard let promoInfo = (entry.info as? PromoChatListItem).flatMap(EngineChatList.AdditionalItem.PromoInfo.init) else {
return nil
}
self.init(item: item, promoInfo: promoInfo)
}
}
public extension EngineChatList {
convenience init(_ view: ChatListView, accountPeerId: PeerId) {
var isLoading = false
var displaySavedMessagesAsTopicList = false
if let value = view.displaySavedMessagesAsTopicList?.get(EngineDisplaySavedChatsAsTopics.self) {
displaySavedMessagesAsTopicList = value.value
}
let isPremium = view.accountPeer?.isPremium ?? false
var items: [EngineChatList.Item] = []
loop: for entry in view.entries {
switch entry {
case .MessageEntry:
if let item = EngineChatList.Item(entry, isPremium: isPremium, displayAsTopicList: entry.index.messageIndex.id.peerId == accountPeerId ? displaySavedMessagesAsTopicList : false) {
items.append(item)
}
case .HoleEntry:
isLoading = true
break loop
}
}
self.init(
items: items,
groupItems: view.groupEntries.map(EngineChatList.GroupItem.init),
additionalItems: view.additionalItemEntries.compactMap(EngineChatList.AdditionalItem.init),
hasEarlier: view.earlierIndex != nil,
hasLater: view.laterIndex != nil,
isLoading: isLoading
)
}
}
@@ -0,0 +1,83 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
func _internal_clearCloudDraftsInteractively(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal<Void, NoError> {
return network.request(Api.functions.messages.getAllDrafts())
|> retryRequest
|> mapToSignal { updates -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Signal<Void, NoError> in
struct Key: Hashable {
var peerId: PeerId
var threadId: Int64?
}
var keys = Set<Key>()
switch updates {
case let .updates(updates, users, chats, _, _):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
for update in updates {
switch update {
case let .updateDraftMessage(_, peer, topMsgId, savedPeerId, _):
var threadId: Int64?
if let savedPeerId {
threadId = savedPeerId.peerId.toInt64()
} else if let topMsgId {
threadId = Int64(topMsgId)
}
keys.insert(Key(peerId: peer.peerId, threadId: threadId))
default:
break
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var signals: [Signal<Void, NoError>] = []
for key in keys {
_internal_updateChatInputState(transaction: transaction, peerId: key.peerId, threadId: key.threadId, inputState: nil)
if let peer = transaction.getPeer(key.peerId), let inputPeer = apiInputPeer(peer) {
var topMsgId: Int32?
var monoforumPeerId: Api.InputPeer?
if let threadId = key.threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
} else {
topMsgId = Int32(clamping: threadId)
}
}
var flags: Int32 = 0
var replyTo: Api.InputReplyTo?
if let topMsgId {
flags |= (1 << 0)
var innerFlags: Int32 = 0
innerFlags |= 1 << 0
replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: 0, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil)
} else if let monoforumPeerId {
flags |= (1 << 0)
replyTo = .inputReplyToMonoForum(monoforumPeerId: monoforumPeerId)
}
signals.append(network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: "", entities: nil, media: nil, effect: nil, suggestedPost: nil))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
})
}
}
return combineLatest(signals)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
default:
break
}
return .complete()
} |> switchToLatest
}
}
@@ -0,0 +1,217 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
func addMessageMediaResourceIdsToRemove(media: Media, resourceIds: inout [MediaResourceId]) {
if let image = media as? TelegramMediaImage {
for representation in image.representations {
resourceIds.append(representation.resource.id)
}
} else if let file = media as? TelegramMediaFile {
for representation in file.previewRepresentations {
resourceIds.append(representation.resource.id)
}
resourceIds.append(file.resource.id)
}
}
func addMessageMediaResourceIdsToRemove(message: Message, resourceIds: inout [MediaResourceId]) {
for media in message.media {
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
}
}
public func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageThreadKey, Int, Int) -> Void)? = nil) {
var resourceIds: [MediaResourceId] = []
if deleteMedia {
for id in ids {
if id.peerId.namespace == Namespaces.Peer.SecretChat {
if let message = transaction.getMessage(id) {
addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds)
}
}
}
}
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
for id in ids {
if id.peerId.namespace == Namespaces.Peer.CloudChannel && id.namespace == Namespaces.Message.Cloud {
if let message = transaction.getMessage(id) {
if let threadId = message.threadId {
let messageThreadKey = MessageThreadKey(peerId: message.id.peerId, threadId: threadId)
if id.peerId.namespace == Namespaces.Peer.CloudChannel {
if let manualAddMessageThreadStatsDifference = manualAddMessageThreadStatsDifference {
manualAddMessageThreadStatsDifference(messageThreadKey, 0, 1)
} else {
updateMessageThreadStats(transaction: transaction, threadKey: messageThreadKey, removedCount: 1, addedMessagePeers: [])
}
}
}
}
}
}
transaction.deleteMessages(ids, forEachMedia: { _ in
})
}
func _internal_deleteAllMessagesWithAuthor(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, authorId: PeerId, namespace: MessageId.Namespace) {
var resourceIds: [MediaResourceId] = []
transaction.removeAllMessagesWithAuthor(peerId, authorId: authorId, namespace: namespace, forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds))).start()
}
}
func _internal_deleteAllMessagesWithForwardAuthor(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, forwardAuthorId: PeerId, namespace: MessageId.Namespace) {
var resourceIds: [MediaResourceId] = []
transaction.removeAllMessagesWithForwardAuthor(peerId, forwardAuthorId: forwardAuthorId, namespace: namespace, forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
}
func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, namespaces: MessageIdNamespaces) {
if peerId.namespace == Namespaces.Peer.SecretChat {
var resourceIds: [MediaResourceId] = []
transaction.withAllMessages(peerId: peerId, { message in
addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds)
return true
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
}
transaction.clearHistory(peerId, threadId: threadId, minTimestamp: nil, maxTimestamp: nil, namespaces: namespaces, forEachMedia: { _ in
})
}
func _internal_clearHistoryInRange(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, minTimestamp: Int32, maxTimestamp: Int32, namespaces: MessageIdNamespaces) {
if peerId.namespace == Namespaces.Peer.SecretChat {
var resourceIds: [MediaResourceId] = []
transaction.withAllMessages(peerId: peerId, { message in
if message.timestamp >= minTimestamp && message.timestamp <= maxTimestamp {
addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds)
}
return true
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
}
transaction.clearHistory(peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: { _ in
})
}
public enum ClearCallHistoryError {
case generic
}
func _internal_clearCallHistory(account: Account, forEveryone: Bool) -> Signal<Never, ClearCallHistoryError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
var flags: Int32 = 0
if forEveryone {
flags |= 1 << 0
}
let signal = account.network.request(Api.functions.messages.deletePhoneCallHistory(flags: flags))
|> map { result -> Api.messages.AffectedFoundMessages? in
return result
}
|> `catch` { _ -> Signal<Api.messages.AffectedFoundMessages?, Bool> in
return .fail(false)
}
|> mapToSignal { result -> Signal<Void, Bool> in
if let result = result {
switch result {
case let .affectedFoundMessages(pts, ptsCount, offset, _):
account.stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
if offset == 0 {
return .fail(true)
} else {
return .complete()
}
}
} else {
return .fail(true)
}
}
return (signal
|> restart)
|> `catch` { success -> Signal<Void, NoError> in
if success {
return account.postbox.transaction { transaction -> Void in
transaction.removeAllMessagesWithGlobalTag(tag: GlobalMessageTags.Calls)
}
} else {
return .complete()
}
}
}
|> switchToLatest
|> ignoreValues
|> castError(ClearCallHistoryError.self)
}
public enum SetChatMessageAutoremoveTimeoutError {
case generic
}
func _internal_setChatMessageAutoremoveTimeoutInteractively(account: Account, peerId: PeerId, timeout: Int32?) -> Signal<Never, SetChatMessageAutoremoveTimeoutError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> castError(SetChatMessageAutoremoveTimeoutError.self)
|> mapToSignal { inputPeer -> Signal<Never, SetChatMessageAutoremoveTimeoutError> in
guard let inputPeer = inputPeer else {
return .fail(.generic)
}
return account.network.request(Api.functions.messages.setHistoryTTL(peer: inputPeer, period: timeout ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> castError(SetChatMessageAutoremoveTimeoutError.self)
|> mapToSignal { result -> Signal<Never, SetChatMessageAutoremoveTimeoutError> in
if let result = result {
account.stateManager.addUpdates(result)
return account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in
let updatedTimeout: CachedPeerAutoremoveTimeout
if let timeout = timeout {
updatedTimeout = .known(CachedPeerAutoremoveTimeout.Value(peerValue: timeout))
} else {
updatedTimeout = .known(nil)
}
if peerId.namespace == Namespaces.Peer.CloudUser {
let current = (current as? CachedUserData) ?? CachedUserData()
return current.withUpdatedAutoremoveTimeout(updatedTimeout)
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
let current = (current as? CachedChannelData) ?? CachedChannelData()
return current.withUpdatedAutoremoveTimeout(updatedTimeout)
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
let current = (current as? CachedGroupData) ?? CachedGroupData()
return current.withUpdatedAutoremoveTimeout(updatedTimeout)
} else {
return current
}
})
}
|> castError(SetChatMessageAutoremoveTimeoutError.self)
|> ignoreValues
} else {
return .fail(.generic)
}
}
|> `catch` { _ -> Signal<Never, SetChatMessageAutoremoveTimeoutError> in
return .complete()
}
}
}
@@ -0,0 +1,250 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func _internal_deleteMessagesInteractively(account: Account, messageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Void in
deleteMessagesInteractively(transaction: transaction, stateManager: account.stateManager, postbox: account.postbox, messageIds: messageIds, type: type, removeIfPossiblyDelivered: true)
}
}
func deleteMessagesInteractively(transaction: Transaction, stateManager: AccountStateManager?, postbox: Postbox, messageIds initialMessageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false, removeIfPossiblyDelivered: Bool) {
var messageIds: [MessageAndThreadId] = []
if deleteAllInGroup {
var tempIds: [MessageId] = initialMessageIds
for id in initialMessageIds {
if let group = transaction.getMessageGroup(id) ?? transaction.getMessageForwardedGroup(id) {
for message in group {
if !tempIds.contains(message.id) {
tempIds.append(message.id)
}
}
} else {
tempIds.append(id)
}
}
messageIds = tempIds.map { id in
if id.namespace == Namespaces.Message.QuickReplyCloud {
if let message = transaction.getMessage(id) {
return MessageAndThreadId(messageId: id, threadId: message.threadId)
} else {
return MessageAndThreadId(messageId: id, threadId: nil)
}
} else {
return MessageAndThreadId(messageId: id, threadId: nil)
}
}
} else {
messageIds = initialMessageIds.map { id in
if id.namespace == Namespaces.Message.QuickReplyCloud {
if let message = transaction.getMessage(id) {
return MessageAndThreadId(messageId: id, threadId: message.threadId)
} else {
return MessageAndThreadId(messageId: id, threadId: nil)
}
} else {
return MessageAndThreadId(messageId: id, threadId: nil)
}
}
}
var uniqueIds: [Int64: PeerId] = [:]
for (peerAndThreadId, peerMessageIds) in messagesIdsGroupedByPeerId(messageIds) {
let peerId = peerAndThreadId.peerId
let threadId = peerAndThreadId.threadId
for id in peerMessageIds {
if let message = transaction.getMessage(id) {
for attribute in message.attributes {
if let attribute = attribute as? OutgoingMessageInfoAttribute {
uniqueIds[attribute.uniqueId] = peerId
}
}
}
}
if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudUser {
let remoteMessageIds = peerMessageIds.filter { id in
if id.namespace == Namespaces.Message.Local || id.namespace == Namespaces.Message.ScheduledLocal || id.namespace == Namespaces.Message.QuickReplyLocal {
return false
}
return true
}
if !remoteMessageIds.isEmpty {
cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: remoteMessageIds, type: CloudChatRemoveMessagesType(type))
}
} else if peerId.namespace == Namespaces.Peer.SecretChat {
if let state = transaction.getPeerChatState(peerId) as? SecretChatState {
var layer: SecretChatLayer?
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
if let layer = layer {
var globallyUniqueIds: [Int64] = []
for messageId in peerMessageIds {
if let message = transaction.getMessage(messageId), let globallyUniqueId = message.globallyUniqueId {
globallyUniqueIds.append(globallyUniqueId)
}
}
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.deleteMessages(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: globallyUniqueIds), state: state)
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
}
}
}
}
_internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: messageIds.map(\.messageId))
stateManager?.notifyDeletedMessages(messageIds: messageIds.map(\.messageId))
if !uniqueIds.isEmpty && removeIfPossiblyDelivered {
stateManager?.removePossiblyDeliveredMessages(uniqueIds: uniqueIds)
}
}
func _internal_clearHistoryInRangeInteractively(postbox: Postbox, peerId: PeerId, threadId: Int64?, minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel {
cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, threadId: threadId, explicitTopMessageId: nil, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: CloudChatClearHistoryType(type))
if type == .scheduledMessages {
} else {
_internal_clearHistoryInRange(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: .not(Namespaces.Message.allNonRegular))
}
} else if peerId.namespace == Namespaces.Peer.SecretChat {
/*_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .all)
if let state = transaction.getPeerChatState(peerId) as? SecretChatState {
var layer: SecretChatLayer?
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
if let layer = layer {
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.clearHistory(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max)), state: state)
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
}
}*/
}
}
}
func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, threadId: Int64?, type: InteractiveHistoryClearingType) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel {
cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, threadId: threadId, explicitTopMessageId: nil, minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type))
if type == .scheduledMessages {
_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, namespaces: .just(Namespaces.Message.allScheduled))
} else {
var topIndex: MessageIndex?
if let topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud), let topMessage = transaction.getMessage(topMessageId) {
topIndex = topMessage.index
}
_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, namespaces: .not(Namespaces.Message.allNonRegular))
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, let migrationReference = cachedData.migrationReference {
cloudChatAddClearHistoryOperation(transaction: transaction, peerId: migrationReference.maxMessageId.peerId, threadId: threadId, explicitTopMessageId: MessageId(peerId: migrationReference.maxMessageId.peerId, namespace: migrationReference.maxMessageId.namespace, id: migrationReference.maxMessageId.id + 1), minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type))
_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: migrationReference.maxMessageId.peerId, threadId: threadId, namespaces: .all)
}
if let topIndex = topIndex {
if peerId.namespace == Namespaces.Peer.CloudUser {
var addEmptyMessage = false
if threadId == nil {
addEmptyMessage = true
} else {
if transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) == nil {
addEmptyMessage = true
}
}
if addEmptyMessage {
let _ = transaction.addMessages([StoreMessage(id: topIndex.id, customStableId: nil, globallyUniqueId: nil, groupingKey: nil, threadId: nil, timestamp: topIndex.timestamp, flags: StoreMessageFlags(), tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: nil, text: "", attributes: [], media: [TelegramMediaAction(action: .historyCleared)])], location: .Random)
}
} else {
updatePeerChatInclusionWithMinTimestamp(transaction: transaction, id: peerId, minTimestamp: topIndex.timestamp, forceRootGroupIfNotExists: false)
}
}
}
} else if peerId.namespace == Namespaces.Peer.SecretChat {
_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: nil, namespaces: .all)
if let state = transaction.getPeerChatState(peerId) as? SecretChatState {
var layer: SecretChatLayer?
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
if let layer = layer {
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.clearHistory(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max)), state: state)
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
}
}
}
}
}
func _internal_clearAuthorHistory(account: Account, peerId: PeerId, memberId: PeerId) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
if let peer = transaction.getPeer(peerId), let memberPeer = transaction.getPeer(memberId), let inputChannel = apiInputChannel(peer), let inputUser = apiInputPeer(memberPeer) {
let signal = peer.isMonoForum ? .fail(true) : account.network.request(Api.functions.channels.deleteParticipantHistory(channel: inputChannel, participant: inputUser))
|> map { result -> Api.messages.AffectedHistory? in
return result
}
|> `catch` { _ -> Signal<Api.messages.AffectedHistory?, Bool> in
return .fail(false)
}
|> mapToSignal { result -> Signal<Void, Bool> in
if let result = result {
switch result {
case let .affectedHistory(pts, ptsCount, offset):
account.stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
if offset == 0 {
return .fail(true)
} else {
return .complete()
}
}
} else {
return .fail(true)
}
}
return (signal
|> restart)
|> `catch` { success -> Signal<Void, NoError> in
if success {
return account.postbox.transaction { transaction -> Void in
_internal_deleteAllMessagesWithAuthor(transaction: transaction, mediaBox: account.postbox.mediaBox, peerId: peerId, authorId: memberId, namespace: Namespaces.Message.Cloud)
}
} else {
return .complete()
}
}
} else {
return .complete()
}
} |> switchToLatest
}
@@ -0,0 +1,144 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
public enum EarliestUnseenPersonalMentionMessageResult: Equatable {
case loading
case result(MessageId?)
}
func _internal_earliestUnseenPersonalMentionMessage(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<EarliestUnseenPersonalMentionMessageResult, NoError> {
return account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: threadId), index: .lowerBound, anchorIndex: .lowerBound, count: 4, fixedCombinedReadStates: nil, tag: .tag(.unseenPersonalMessage), additionalData: [.peerChatState(peerId)])
|> mapToSignal { view -> Signal<EarliestUnseenPersonalMentionMessageResult, NoError> in
if view.0.isLoading {
return .single(.loading)
}
if case .FillHole = view.1 {
return _internal_earliestUnseenPersonalMentionMessage(account: account, peerId: peerId, threadId: threadId)
}
if let message = view.0.entries.first?.message {
if peerId.namespace == Namespaces.Peer.CloudChannel {
var invalidatedPts: Int32?
for data in view.0.additionalData {
switch data {
case let .peerChatState(_, state):
if let state = state as? ChannelState {
invalidatedPts = state.invalidatedPts
}
default:
break
}
}
if let invalidatedPts = invalidatedPts {
var messagePts: Int32?
for attribute in message.attributes {
if let attribute = attribute as? ChannelMessageStateVersionAttribute {
messagePts = attribute.pts
break
}
}
if let messagePts = messagePts {
if messagePts < invalidatedPts {
return .single(.loading)
}
}
}
return .single(.result(message.id))
} else {
return .single(.result(message.id))
}
} else {
return account.postbox.transaction { transaction -> EarliestUnseenPersonalMentionMessageResult in
if let topId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) {
transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, customTag: nil, count: 0, maxId: topId.id)
transaction.removeHole(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, space: .tag(.unseenPersonalMessage), range: 1 ... (Int32.max - 1))
let ids = transaction.getMessageIndicesWithTag(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, tag: .unseenPersonalMessage).map({ $0.id })
for id in ids {
markUnseenPersonalMessage(transaction: transaction, id: id, addSynchronizeAction: false)
}
}
return .result(nil)
}
}
}
|> distinctUntilChanged
|> take(until: { value in
if case .result = value {
return SignalTakeAction(passthrough: true, complete: true)
} else {
return SignalTakeAction(passthrough: true, complete: false)
}
})
}
func _internal_earliestUnseenPersonalReactionMessage(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<EarliestUnseenPersonalMentionMessageResult, NoError> {
return account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: threadId), index: .lowerBound, anchorIndex: .lowerBound, count: 4, fixedCombinedReadStates: nil, tag: .tag(.unseenReaction), additionalData: [.peerChatState(peerId)])
|> mapToSignal { view -> Signal<EarliestUnseenPersonalMentionMessageResult, NoError> in
if view.0.isLoading {
return .single(.loading)
}
if case .FillHole = view.1 {
return _internal_earliestUnseenPersonalReactionMessage(account: account, peerId: peerId, threadId: threadId)
}
if let message = view.0.entries.first?.message {
if peerId.namespace == Namespaces.Peer.CloudChannel {
var invalidatedPts: Int32?
for data in view.0.additionalData {
switch data {
case let .peerChatState(_, state):
if let state = state as? ChannelState {
invalidatedPts = state.invalidatedPts
}
default:
break
}
}
if let invalidatedPts = invalidatedPts {
var messagePts: Int32?
for attribute in message.attributes {
if let attribute = attribute as? ChannelMessageStateVersionAttribute {
messagePts = attribute.pts
break
}
}
if let messagePts = messagePts {
if messagePts < invalidatedPts {
return .single(.loading)
}
}
}
return .single(.result(message.id))
} else {
return .single(.result(message.id))
}
} else {
return account.postbox.transaction { transaction -> EarliestUnseenPersonalMentionMessageResult in
if let topId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) {
transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud, customTag: nil, count: 0, maxId: topId.id)
transaction.removeHole(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, space: .tag(.unseenReaction), range: 1 ... (Int32.max - 1))
let ids = transaction.getMessageIndicesWithTag(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, tag: .unseenReaction).map({ $0.id })
for id in ids {
markUnseenReactionMessage(transaction: transaction, id: id, addSynchronizeAction: false)
}
}
return .result(nil)
}
}
}
|> distinctUntilChanged
|> take(until: { value in
if case .result = value {
return SignalTakeAction(passthrough: true, complete: true)
} else {
return SignalTakeAction(passthrough: true, complete: false)
}
})
}
@@ -0,0 +1,39 @@
import Postbox
public final class EngineGroupCallDescription {
public let id: Int64
public let accessHash: Int64
public let title: String?
public let scheduleTimestamp: Int32?
public let subscribedToScheduled: Bool
public let isStream: Bool?
public init(
id: Int64,
accessHash: Int64,
title: String?,
scheduleTimestamp: Int32?,
subscribedToScheduled: Bool,
isStream: Bool?
) {
self.id = id
self.accessHash = accessHash
self.title = title
self.scheduleTimestamp = scheduleTimestamp
self.subscribedToScheduled = subscribedToScheduled
self.isStream = isStream
}
}
public extension EngineGroupCallDescription {
convenience init(_ activeCall: CachedChannelData.ActiveCall) {
self.init(
id: activeCall.id,
accessHash: activeCall.accessHash,
title: activeCall.title,
scheduleTimestamp: activeCall.scheduleTimestamp,
subscribedToScheduled: activeCall.subscribedToScheduled,
isStream: activeCall.isStream
)
}
}
@@ -0,0 +1,904 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
public final class EngineStoryViewListContext {
public struct LoadMoreToken: Equatable {
var value: String
}
public enum ListMode {
case everyone
case contacts
}
public enum SortMode {
case repostsFirst
case reactionsFirst
case recentFirst
}
public enum Item: Equatable {
public final class View: Equatable {
public let peer: EnginePeer
public let timestamp: Int32
public let storyStats: PeerStoryStats?
public let reaction: MessageReaction.Reaction?
public let reactionFile: TelegramMediaFile?
public init(
peer: EnginePeer,
timestamp: Int32,
storyStats: PeerStoryStats?,
reaction: MessageReaction.Reaction?,
reactionFile: TelegramMediaFile?
) {
self.peer = peer
self.timestamp = timestamp
self.storyStats = storyStats
self.reaction = reaction
self.reactionFile = reactionFile
}
public static func ==(lhs: View, rhs: View) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.timestamp != rhs.timestamp {
return false
}
if lhs.storyStats != rhs.storyStats {
return false
}
if lhs.reaction != rhs.reaction {
return false
}
if lhs.reactionFile?.fileId != rhs.reactionFile?.fileId {
return false
}
return true
}
}
public final class Repost: Equatable {
public let peer: EnginePeer
public let story: EngineStoryItem
public let storyStats: PeerStoryStats?
init(peer: EnginePeer, story: EngineStoryItem, storyStats: PeerStoryStats?) {
self.peer = peer
self.story = story
self.storyStats = storyStats
}
public static func ==(lhs: Repost, rhs: Repost) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.story != rhs.story {
return false
}
if lhs.storyStats != rhs.storyStats {
return false
}
return true
}
}
public final class Forward: Equatable {
public let message: EngineMessage
public let storyStats: PeerStoryStats?
init(message: EngineMessage, storyStats: PeerStoryStats?) {
self.message = message
self.storyStats = storyStats
}
public static func ==(lhs: Forward, rhs: Forward) -> Bool {
if lhs.message != rhs.message {
return false
}
if lhs.storyStats != rhs.storyStats {
return false
}
return true
}
}
case view(View)
case repost(Repost)
case forward(Forward)
public var peer: EnginePeer {
switch self {
case let .view(view):
return view.peer
case let .repost(repost):
return repost.peer
case let .forward(forward):
return EnginePeer(forward.message.peers[forward.message.id.peerId]!)
}
}
public var timestamp: Int32 {
switch self {
case let .view(view):
return view.timestamp
case let .repost(repost):
return repost.story.timestamp
case let .forward(forward):
return forward.message.timestamp
}
}
public var reaction: MessageReaction.Reaction? {
switch self {
case let .view(view):
return view.reaction
case .repost:
return nil
case .forward:
return nil
}
}
public var story: EngineStoryItem? {
switch self {
case .view:
return nil
case let .repost(repost):
return repost.story
case .forward:
return nil
}
}
public var message: EngineMessage? {
switch self {
case .view:
return nil
case .repost:
return nil
case let .forward(forward):
return forward.message
}
}
public var storyStats: PeerStoryStats? {
switch self {
case let .view(view):
return view.storyStats
case let .repost(repost):
return repost.storyStats
case let .forward(forward):
return forward.storyStats
}
}
public struct ItemHash: Hashable {
public var peerId: EnginePeer.Id
public var storyId: Int32?
public var messageId: EngineMessage.Id?
}
public var uniqueId: ItemHash {
switch self {
case let .view(view):
return ItemHash(peerId: view.peer.id, storyId: nil, messageId: nil)
case let .repost(repost):
return ItemHash(peerId: repost.peer.id, storyId: repost.story.id, messageId: nil)
case let .forward(forward):
return ItemHash(peerId: forward.message.id.peerId, storyId: nil, messageId: forward.message.id)
}
}
}
public struct State: Equatable {
public var totalCount: Int
public var totalReactedCount: Int
public var items: [Item]
public var loadMoreToken: LoadMoreToken?
public init(
totalCount: Int,
totalReactedCount: Int,
items: [Item],
loadMoreToken: LoadMoreToken?
) {
self.totalCount = totalCount
self.totalReactedCount = totalReactedCount
self.items = items
self.loadMoreToken = loadMoreToken
}
}
private final class Impl {
struct NextOffset: Equatable {
var value: String
}
struct InternalState: Equatable {
var totalCount: Int
var totalViewsCount: Int
var totalForwardsCount: Int
var totalReactedCount: Int
var items: [Item]
var canLoadMore: Bool
var nextOffset: NextOffset?
}
let queue: Queue
let account: Account
let peerId: EnginePeer.Id
let storyId: Int32
let listMode: ListMode
let sortMode: SortMode
let searchQuery: String?
let disposable = MetaDisposable()
let storyStatsDisposable = MetaDisposable()
var state: InternalState?
let statePromise = Promise<InternalState>()
private var parentSource: Impl?
var isLoadingMore: Bool = false
init(queue: Queue, account: Account, peerId: EnginePeer.Id, storyId: Int32, views: EngineStoryItem.Views, listMode: ListMode, sortMode: SortMode, searchQuery: String?, parentSource: Impl?) {
self.queue = queue
self.account = account
self.peerId = peerId
self.storyId = storyId
self.listMode = listMode
self.sortMode = sortMode
self.searchQuery = searchQuery
if let parentSource = parentSource, (parentSource.listMode == .everyone || parentSource.listMode == listMode), let parentState = parentSource.state, parentState.totalCount <= 100 {
self.parentSource = parentSource
let matchesMode = parentSource.listMode == listMode
if parentState.items.count < 100 && !matchesMode {
parentSource.loadMore()
}
self.disposable.set((parentSource.statePromise.get()
|> mapToSignal { state -> Signal<InternalState, NoError> in
let needUpdate: Signal<Void, NoError>
if listMode == .contacts {
var keys: [PostboxViewKey] = []
for item in state.items {
keys.append(.isContact(id: item.peer.id))
}
needUpdate = account.postbox.combinedView(keys: keys)
|> map { views -> [Bool] in
var result: [Bool] = []
for item in state.items {
if let view = views.views[.isContact(id: item.peer.id)] as? IsContactView {
result.append(view.isContact)
}
}
return result
}
|> distinctUntilChanged
|> map { _ -> Void in
return Void()
}
} else {
needUpdate = .single(Void())
}
return needUpdate
|> mapToSignal { _ -> Signal<InternalState, NoError> in
return account.postbox.transaction { transaction -> InternalState in
/*if state.canLoadMore && !matchesMode {
return InternalState(
totalCount: listMode == .everyone ? state.totalCount : 100, totalReactedCount: state.totalReactedCount, items: [], canLoadMore: true, nextOffset: state.nextOffset)
}*/
var items: [Item] = []
switch listMode {
case .everyone:
items = state.items
case .contacts:
items = state.items.filter { item in
return transaction.isPeerContact(peerId: item.peer.id)
}
}
if let searchQuery = searchQuery, !searchQuery.isEmpty {
let normalizedQuery = searchQuery.lowercased()
items = state.items.filter { item in
return item.peer.indexName.matchesByTokens(normalizedQuery)
}
}
switch sortMode {
case .repostsFirst:
items.sort(by: { lhs, rhs in
if (lhs.story == nil) != (rhs.story == nil) {
return lhs.story != nil
}
if (lhs.message == nil) != (rhs.message == nil) {
return lhs.message != nil
}
if lhs.timestamp != rhs.timestamp {
return lhs.timestamp > rhs.timestamp
}
return lhs.peer.id < rhs.peer.id
})
case .reactionsFirst:
items.sort(by: { lhs, rhs in
if (lhs.story == nil) != (rhs.story == nil) {
return lhs.story == nil
}
if (lhs.message == nil) != (rhs.message == nil) {
return lhs.message == nil
}
if (lhs.reaction == nil) != (rhs.reaction == nil) {
return lhs.reaction != nil
}
if lhs.timestamp != rhs.timestamp {
return lhs.timestamp > rhs.timestamp
}
return lhs.peer.id < rhs.peer.id
})
case .recentFirst:
items.sort(by: { lhs, rhs in
if lhs.timestamp != rhs.timestamp {
return lhs.timestamp > rhs.timestamp
}
return lhs.peer.id < rhs.peer.id
})
}
var totalCount = items.count
var totalReactedCount = 0
for item in items {
if item.reaction != nil {
totalReactedCount += 1
}
}
if state.canLoadMore {
totalCount = state.totalCount
totalReactedCount = state.totalReactedCount
}
return InternalState(
totalCount: totalCount,
totalViewsCount: 0,
totalForwardsCount: 0,
totalReactedCount: totalReactedCount,
items: items,
canLoadMore: state.canLoadMore
)
}
}
}
|> deliverOn(self.queue)).start(next: { [weak self] state in
guard let `self` = self else {
return
}
self.updateInternalState(state: state)
}))
} else {
let initialState = State(totalCount: listMode == .everyone ? views.seenCount : 100, totalReactedCount: views.reactedCount, items: [], loadMoreToken: LoadMoreToken(value: ""))
let state = InternalState(totalCount: initialState.totalCount, totalViewsCount: initialState.totalCount, totalForwardsCount: initialState.totalCount, totalReactedCount: initialState.totalReactedCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil)
self.state = state
self.statePromise.set(.single(state))
if initialState.loadMoreToken != nil {
self.loadMore()
}
}
}
deinit {
assert(self.queue.isCurrent())
self.disposable.dispose()
self.storyStatsDisposable.dispose()
}
func loadMore() {
if let parentSource = self.parentSource {
parentSource.loadMore()
return
}
guard let state = self.state else {
return
}
if !state.canLoadMore {
return
}
if self.isLoadingMore {
return
}
self.isLoadingMore = true
let account = self.account
let accountPeerId = account.peerId
let peerId = self.peerId
let storyId = self.storyId
let listMode = self.listMode
let sortMode = self.sortMode
let searchQuery = self.searchQuery
let currentOffset = state.nextOffset
let limit = 50
let signal: Signal<InternalState, NoError>
if peerId.namespace == Namespaces.Peer.CloudUser {
signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<InternalState, NoError> in
guard let inputPeer = inputPeer else {
return .complete()
}
var flags: Int32 = 0
switch listMode {
case .everyone:
break
case .contacts:
flags |= (1 << 0)
}
switch sortMode {
case .reactionsFirst:
flags |= (1 << 2)
case .recentFirst, .repostsFirst:
break
}
if searchQuery != nil {
flags |= (1 << 1)
}
return account.network.request(Api.functions.stories.getStoryViewsList(flags: flags, peer: inputPeer, q: searchQuery, id: storyId, offset: currentOffset?.value ?? "", limit: Int32(limit)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.stories.StoryViewsList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<InternalState, NoError> in
return account.postbox.transaction { transaction -> InternalState in
switch result {
case let .storyViewsList(_, count, viewsCount, forwardsCount, reactionsCount, views, chats, users, nextOffset):
let peers = AccumulatedPeers(chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers)
var items: [Item] = []
for view in views {
switch view {
case let .storyView(flags, userId, date, reaction):
let isBlocked = (flags & (1 << 0)) != 0
let isBlockedFromStories = (flags & (1 << 1)) != 0
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData in
let previousData: CachedUserData
if let current = cachedData as? CachedUserData {
previousData = current
} else {
previousData = CachedUserData()
}
var updatedFlags = previousData.flags
if isBlockedFromStories {
updatedFlags.insert(.isBlockedFromStories)
} else {
updatedFlags.remove(.isBlockedFromStories)
}
return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags)
})
if let peer = transaction.getPeer(peerId) {
let parsedReaction = reaction.flatMap(MessageReaction.Reaction.init(apiReaction:))
items.append(.view(Item.View(
peer: EnginePeer(peer),
timestamp: date,
storyStats: transaction.getPeerStoryStats(peerId: peerId),
reaction: parsedReaction,
reactionFile: parsedReaction.flatMap { reaction -> TelegramMediaFile? in
switch reaction {
case .builtin:
return nil
case let .custom(fileId):
return transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile
case .stars:
return nil
}
}
)))
}
case let .storyViewPublicForward(flags, message):
let _ = flags
if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: false), let message = locallyRenderedMessage(message: storeMessage, peers: peers.peers) {
items.append(.forward(Item.Forward(
message: EngineMessage(message),
storyStats: transaction.getPeerStoryStats(peerId: message.id.peerId)
)))
}
case let .storyViewPublicRepost(flags, peerId, story):
let _ = flags
if let peer = transaction.getPeer(peerId.peerId) {
if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peer.id, transaction: transaction), case let .item(item) = storedItem, let media = item.media {
items.append(.repost(Item.Repost(
peer: EnginePeer(peer),
story: 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
),
storyStats: transaction.getPeerStoryStats(peerId: peer.id)
)))
}
}
}
}
if listMode == .everyone, searchQuery == nil {
if let storedItem = transaction.getStory(id: StoryId(peerId: account.peerId, id: storyId))?.get(Stories.StoredItem.self), case let .item(item) = storedItem, let currentViews = item.views {
let updatedItem: Stories.StoredItem = .item(Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
alternativeMediaList: item.alternativeMediaList,
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: Stories.Item.Views(
seenCount: Int(count),
reactedCount: Int(reactionsCount),
forwardCount: Int(forwardsCount),
seenPeerIds: currentViews.seenPeerIds,
reactions: currentViews.reactions,
hasList: currentViews.hasList
),
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction,
forwardInfo: item.forwardInfo,
authorId: item.authorId,
folderIds: item.folderIds
))
if let entry = CodableEntry(updatedItem) {
transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry)
}
}
var currentItems = transaction.getStoryItems(peerId: account.peerId)
for i in 0 ..< currentItems.count {
if currentItems[i].id == storyId {
if case let .item(item) = currentItems[i].value.get(Stories.StoredItem.self), let currentViews = item.views {
let updatedItem: Stories.StoredItem = .item(Stories.Item(
id: item.id,
timestamp: item.timestamp,
expirationTimestamp: item.expirationTimestamp,
media: item.media,
alternativeMediaList: item.alternativeMediaList,
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: Stories.Item.Views(
seenCount: Int(count),
reactedCount: Int(reactionsCount),
forwardCount: Int(forwardsCount),
seenPeerIds: currentViews.seenPeerIds,
reactions: currentViews.reactions,
hasList: currentViews.hasList
),
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isContacts: item.isContacts,
isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction,
forwardInfo: item.forwardInfo,
authorId: item.authorId,
folderIds: item.folderIds
))
if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends, isLiveStream: updatedItem.isLiveStream)
}
}
}
}
transaction.setStoryItems(peerId: account.peerId, items: currentItems)
}
return InternalState(totalCount: Int(count), totalViewsCount: Int(viewsCount), totalForwardsCount: Int(forwardsCount), totalReactedCount: Int(reactionsCount), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset.flatMap { NextOffset(value: $0) })
case .none:
return InternalState(totalCount: 0, totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil)
}
}
}
}
} else {
signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<InternalState, NoError> in
guard let inputPeer = inputPeer else {
return .complete()
}
var flags: Int32 = 0
if let _ = currentOffset {
flags |= (1 << 1)
}
if case .repostsFirst = sortMode {
flags |= (1 << 2)
}
return account.network.request(Api.functions.stories.getStoryReactionsList(flags: flags, peer: inputPeer, id: storyId, reaction: nil, offset: currentOffset?.value, limit: Int32(limit)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.stories.StoryReactionsList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<InternalState, NoError> in
return account.postbox.transaction { transaction -> InternalState in
switch result {
case let .storyReactionsList(_, count, reactions, chats, users, nextOffset):
let peers = AccumulatedPeers(chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers)
var items: [Item] = []
for reaction in reactions {
switch reaction {
case let .storyReaction(peerId, date, reaction):
if let peer = transaction.getPeer(peerId.peerId) {
if let parsedReaction = MessageReaction.Reaction(apiReaction: reaction) {
let reactionFile: TelegramMediaFile?
switch parsedReaction {
case .builtin:
reactionFile = nil
case let .custom(fileId):
reactionFile = transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile
case .stars:
reactionFile = nil
}
items.append(.view(Item.View(
peer: EnginePeer(peer),
timestamp: date,
storyStats: transaction.getPeerStoryStats(peerId: peer.id),
reaction: parsedReaction,
reactionFile: reactionFile
)))
}
}
case let .storyReactionPublicForward(message):
if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: false), let message = locallyRenderedMessage(message: storeMessage, peers: peers.peers) {
items.append(.forward(Item.Forward(
message: EngineMessage(message),
storyStats: transaction.getPeerStoryStats(peerId: message.id.peerId)
)))
}
case let .storyReactionPublicRepost(peerId, story):
if let peer = transaction.getPeer(peerId.peerId) {
if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peer.id, transaction: transaction), case let .item(item) = storedItem, let media = item.media {
items.append(.repost(Item.Repost(
peer: EnginePeer(peer),
story: 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
),
storyStats: transaction.getPeerStoryStats(peerId: peer.id)
)))
}
}
}
}
return InternalState(totalCount: Int(count), totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset.flatMap { NextOffset(value: $0) })
case .none:
return InternalState(totalCount: 0, totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil)
}
}
}
}
}
self.disposable.set((signal
|> deliverOn(self.queue)).start(next: { [weak self] state in
guard let `self` = self else {
return
}
self.updateInternalState(state: state)
}))
}
private func updateInternalState(state: InternalState) {
var currentState = self.state ?? InternalState(
totalCount: 0, totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil)
if self.parentSource != nil {
currentState.items.removeAll()
}
var existingItems = Set<Item.ItemHash>()
for item in currentState.items {
existingItems.insert(item.uniqueId)
}
for item in state.items {
let itemHash = item.uniqueId
if existingItems.contains(itemHash) {
continue
}
existingItems.insert(itemHash)
currentState.items.append(item)
}
var allReactedCount = 0
for item in currentState.items {
if case let .view(view) = item, view.reaction != nil {
allReactedCount += 1
} else {
break
}
}
if state.canLoadMore {
currentState.totalCount = max(state.totalCount, currentState.items.count)
currentState.totalReactedCount = max(state.totalReactedCount, allReactedCount)
} else {
currentState.totalCount = currentState.items.count
currentState.totalReactedCount = allReactedCount
}
currentState.canLoadMore = state.canLoadMore
currentState.nextOffset = state.nextOffset
self.isLoadingMore = false
self.state = currentState
self.statePromise.set(.single(currentState))
let statsKey: PostboxViewKey = .peerStoryStats(peerIds: Set(currentState.items.map(\.peer.id)))
self.storyStatsDisposable.set((self.account.postbox.combinedView(keys: [statsKey])
|> deliverOn(self.queue)).start(next: { [weak self] views in
guard let `self` = self, var state = self.state else {
return
}
guard let view = views.views[statsKey] as? PeerStoryStatsView else {
return
}
var updated = false
var items = state.items
for i in 0 ..< state.items.count {
let item = items[i]
let value = view.storyStats[item.peer.id]
if case let .view(view) = item, view.storyStats != value {
updated = true
items[i] = .view(Item.View(
peer: view.peer,
timestamp: view.timestamp,
storyStats: value,
reaction: view.reaction,
reactionFile: view.reactionFile
))
}
}
if updated {
state.items = items
self.state = state
self.statePromise.set(.single(state))
}
}))
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public var state: Signal<State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.statePromise.get().start(next: { state in
var loadMoreToken: LoadMoreToken?
if let nextOffset = state.nextOffset {
loadMoreToken = LoadMoreToken(value: nextOffset.value)
}
subscriber.putNext(State(
totalCount: state.totalCount,
totalReactedCount: state.totalReactedCount,
items: state.items,
loadMoreToken: loadMoreToken
))
}))
}
return disposable
}
}
init(account: Account, peerId: EnginePeer.Id, storyId: Int32, views: EngineStoryItem.Views, listMode: ListMode, sortMode: SortMode, searchQuery: String?, parentSource: EngineStoryViewListContext?) {
let queue = Queue.mainQueue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, account: account, peerId: peerId, storyId: storyId, views: views, listMode: listMode, sortMode: sortMode, searchQuery: searchQuery, parentSource: parentSource?.impl.syncWith { $0 })
})
}
public func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
}
@@ -0,0 +1,38 @@
import Postbox
import TelegramApi
import SwiftSignalKit
public func _internal_exportMessageLink(postbox: Postbox, network: Network, peerId: PeerId, messageId: MessageId, isThread: Bool = false) -> Signal<String?, NoError> {
return postbox.transaction { transaction -> (Peer, MessageId)? in
let peer: Peer? = transaction.getPeer(messageId.peerId)
if let peer = peer {
return (peer, messageId)
} else {
return nil
}
}
|> mapToSignal { data -> Signal<String?, NoError> in
guard let (peer, sourceMessageId) = data else {
return .single(nil)
}
if let input = apiInputChannel(peer) {
var flags: Int32 = 0
flags |= 1 << 0
if isThread {
flags |= 1 << 1
}
return network.request(Api.functions.channels.exportMessageLink(flags: flags, channel: input, id: sourceMessageId.id)) |> mapError { _ in return }
|> map { res in
switch res {
case let .exportedMessageLink(link, _):
return link
}
} |> `catch` { _ -> Signal<String?, NoError> in
return .single(nil)
}
} else {
return .single(nil)
}
}
}
@@ -0,0 +1,40 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private func _internal_updateExtendedMediaById(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Peer? in
if let peer = transaction.getPeer(peerId) {
return peer
} else {
return nil
}
}
|> mapToSignal { peer -> Signal<Never, NoError> in
guard let peer = peer, let inputPeer = apiInputPeer(peer) else {
return .complete()
}
return account.network.request(Api.functions.messages.getExtendedMedia(peer: inputPeer, id: messageIds.map { $0.id }))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
}
func _internal_updateExtendedMedia(account: Account, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
var signals: [Signal<Never, NoError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
signals.append(_internal_updateExtendedMediaById(account: account, peerId: peerId, messageIds: messageIds))
}
return combineLatest(signals)
|> ignoreValues
}
@@ -0,0 +1,123 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func _internal_editMessageFactCheck(account: Account, messageId: EngineMessage.Id, text: String, entities: [MessageTextEntity]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer else {
return .complete()
}
return account.network.request(Api.functions.messages.editFactCheck(
peer: inputPeer,
msgId: messageId.id,
text: .textWithEntities(
text: text,
entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary())
)
))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
}
func _internal_deleteMessageFactCheck(account: Account, messageId: EngineMessage.Id) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer else {
return .complete()
}
return account.network.request(Api.functions.messages.deleteFactCheck(peer: inputPeer, msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
}
func _internal_getMessagesFactCheck(account: Account, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
var signals: [Signal<Never, NoError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
signals.append(_internal_getMessagesFactCheckByPeerId(account: account, peerId: peerId, messageIds: messageIds))
}
return combineLatest(signals)
|> ignoreValues
}
func _internal_getMessagesFactCheckByPeerId(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> (Api.InputPeer?, [Message]) in
return (transaction.getPeer(peerId).flatMap(apiInputPeer), messageIds.compactMap({ transaction.getMessage($0) }))
}
|> mapToSignal { (inputPeer, messages) -> Signal<Never, NoError> in
guard let inputPeer = inputPeer else {
return .never()
}
let ids: [Int32] = messageIds.map { $0.id }
let results: Signal<[Api.FactCheck]?, NoError>
if ids.isEmpty {
results = .single(nil)
} else {
results = account.network.request(Api.functions.messages.getFactCheck(peer: inputPeer, msgId: ids))
|> map(Optional.init)
|> `catch` { _ in
return .single(nil)
}
}
return results
|> mapToSignal { results -> Signal<Never, NoError> in
guard let results else {
return .complete()
}
return account.postbox.transaction { transaction in
var index = 0
for result in results {
let messageId = messageIds[index]
switch result {
case let .factCheck(_, country, text, hash):
let content: FactCheckMessageAttribute.Content
if let text, let country {
switch text {
case let .textWithEntities(text, entities):
content = .Loaded(text: text, entities: messageTextEntitiesFromApiEntities(entities), country: country)
}
} else {
content = .Pending
}
let attribute = FactCheckMessageAttribute(content: content, hash: hash)
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is FactCheckMessageAttribute) }
attributes.append(attribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
index += 1
}
}
|> ignoreValues
}
}
}
@@ -0,0 +1,31 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_forwardGameWithScore(account: Account, messageId: MessageId, to peerId: PeerId, threadId: Int64?, as sendAsPeerId: PeerId?) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
if let _ = transaction.getMessage(messageId), let fromPeer = transaction.getPeer(messageId.peerId), let fromInputPeer = apiInputPeer(fromPeer), let toPeer = transaction.getPeer(peerId), let toInputPeer = apiInputPeer(toPeer) {
var flags: Int32 = 1 << 8
var sendAsInputPeer: Api.InputPeer?
if let sendAsPeerId = sendAsPeerId, let sendAsPeer = transaction.getPeer(sendAsPeerId), let inputPeer = apiInputPeerOrSelf(sendAsPeer, accountPeerId: account.peerId) {
sendAsInputPeer = inputPeer
flags |= (1 << 13)
}
return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, replyTo: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, videoTimestamp: nil, allowPaidStars: nil, suggestedPost: nil))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
return .complete()
} |> switchToLatest
}
@@ -0,0 +1,197 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_installInteractiveReadMessagesAction(postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId, threadId: Int64?) -> Disposable {
return postbox.installStoreMessageAction(peerId: peerId, { messages, transaction in
var consumeMessageIds: [MessageId] = []
var readReactionIds: [MessageId] = []
readReactionIds.removeAll()
var readMessageIndexByNamespace: [MessageId.Namespace: MessageIndex] = [:]
for message in messages {
if case let .Id(id) = message.id {
if threadId == nil || message.threadId == threadId {
} else {
continue
}
var hasUnconsumedMention = false
var hasUnconsumedContent = false
var hasUnseenReactions = false
if message.tags.contains(.unseenPersonalMessage) || message.tags.contains(.unseenReaction) {
inner: for attribute in message.attributes {
if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.consumed, !attribute.pending {
hasUnconsumedMention = true
} else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed {
hasUnconsumedContent = true
} else if let attribute = attribute as? ReactionsMessageAttribute, attribute.hasUnseen {
hasUnseenReactions = true
}
}
}
if hasUnconsumedMention && !hasUnconsumedContent {
consumeMessageIds.append(id)
}
if hasUnseenReactions {
//readReactionIds.append(id)
}
if !message.flags.intersection(.IsIncomingMask).isEmpty {
let index = MessageIndex(id: id, timestamp: message.timestamp)
let current = readMessageIndexByNamespace[id.namespace]
if current == nil || current! < index {
readMessageIndexByNamespace[id.namespace] = index
}
}
}
}
for id in Set(consumeMessageIds + readReactionIds) {
transaction.updateMessage(id, update: { currentMessage in
var attributes = currentMessage.attributes
if consumeMessageIds.contains(id) {
mentionsLoop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ConsumablePersonalMentionMessageAttribute {
attributes[j] = ConsumablePersonalMentionMessageAttribute(consumed: attribute.consumed, pending: true)
break mentionsLoop
}
}
}
var tags = currentMessage.tags
if readReactionIds.contains(id) {
reactionsLoop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute {
attributes[j] = attribute.withAllSeen()
break reactionsLoop
}
}
tags.remove(.unseenReaction)
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
if consumeMessageIds.contains(id) {
transaction.setPendingMessageAction(type: .consumeUnseenPersonalMessage, id: id, action: ConsumePersonalMessageAction())
}
if readReactionIds.contains(id) {
transaction.setPendingMessageAction(type: .readReaction, id: id, action: ReadReactionAction())
}
}
for (_, index) in readMessageIndexByNamespace {
if let threadId {
if var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
if index.id.id >= data.maxIncomingReadId {
if let count = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: data.maxIncomingReadId, toIndex: index) {
data.incomingUnreadCount = max(0, data.incomingUnreadCount - Int32(count))
data.maxIncomingReadId = index.id.id
}
if let topMessageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) {
if index.id.id >= topMessageIndex.id.id {
let containingHole = transaction.getThreadIndexHole(peerId: peerId, threadId: threadId, namespace: topMessageIndex.id.namespace, containing: topMessageIndex.id.id)
if let _ = containingHole[.everywhere] {
} else {
data.incomingUnreadCount = 0
}
}
}
data.maxKnownMessageId = max(data.maxKnownMessageId, index.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
}
}
} else {
_internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index)
}
}
})
}
public struct VisibleMessageRange {
public var lowerBound: MessageIndex
public var upperBound: MessageIndex?
public init(lowerBound: MessageIndex, upperBound: MessageIndex?) {
self.lowerBound = lowerBound
self.upperBound = upperBound
}
fileprivate func contains(index: MessageIndex) -> Bool {
if index < lowerBound {
return false
}
if let upperBound = self.upperBound {
if index > upperBound {
return false
}
}
return true
}
}
private final class StoreOrUpdateMessageActionImpl: StoreOrUpdateMessageAction {
private let getVisibleRange: () -> VisibleMessageRange?
private let didReadReactionsInMessages: ([MessageId: [ReactionsMessageAttribute.RecentPeer]]) -> Void
init(getVisibleRange: @escaping () -> VisibleMessageRange?, didReadReactionsInMessages: @escaping ([MessageId: [ReactionsMessageAttribute.RecentPeer]]) -> Void) {
self.getVisibleRange = getVisibleRange
self.didReadReactionsInMessages = didReadReactionsInMessages
}
func addOrUpdate(messages: [StoreMessage], transaction: Transaction) {
var readReactionIds: [MessageId: [ReactionsMessageAttribute.RecentPeer]] = [:]
guard let visibleRange = self.getVisibleRange() else {
return
}
for message in messages {
guard let index = message.index else {
continue
}
if !visibleRange.contains(index: index) {
continue
}
if message.tags.contains(.unseenReaction) {
inner: for attribute in message.attributes {
if let attribute = attribute as? ReactionsMessageAttribute, attribute.hasUnseen {
readReactionIds[index.id] = attribute.recentPeers
break inner
}
}
}
}
for id in readReactionIds.keys {
transaction.updateMessage(id, update: { currentMessage in
var attributes = currentMessage.attributes
reactionsLoop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute {
attributes[j] = attribute.withAllSeen()
break reactionsLoop
}
}
var tags = currentMessage.tags
tags.remove(.unseenReaction)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
transaction.setPendingMessageAction(type: .readReaction, id: id, action: ReadReactionAction())
}
self.didReadReactionsInMessages(readReactionIds)
}
}
func _internal_installInteractiveReadReactionsAction(postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId, getVisibleRange: @escaping () -> VisibleMessageRange?, didReadReactionsInMessages: @escaping ([MessageId: [ReactionsMessageAttribute.RecentPeer]]) -> Void) -> Disposable {
return postbox.installStoreOrUpdateMessageAction(peerId: peerId, action: StoreOrUpdateMessageActionImpl(getVisibleRange: getVisibleRange, didReadReactionsInMessages: didReadReactionsInMessages))
}
@@ -0,0 +1,132 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum GetMessagesResult {
case progress
case result([Message])
}
public enum GetMessagesStrategy {
case local
case cloud(skipLocal: Bool)
}
public enum GetMessagesError {
case privateChannel
}
func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal<GetMessagesResult, GetMessagesError> {
let postboxSignal = postbox.transaction { transaction -> ([Message], Set<MessageId>, SimpleDictionary<PeerId, Peer>) in
var ids = messageIds
if let cachedData = transaction.getPeerCachedData(peerId: messageIds[0].peerId) as? CachedChannelData {
if let minAvailableMessageId = cachedData.minAvailableMessageId {
ids = ids.filter({$0 < minAvailableMessageId})
}
}
var messages:[Message] = []
var missingMessageIds:Set<MessageId> = Set()
var supportPeers: SimpleDictionary<PeerId, Peer> = SimpleDictionary()
for messageId in ids {
if case let .cloud(skipLocal) = strategy, skipLocal {
missingMessageIds.insert(messageId)
if let peer = transaction.getPeer(messageId.peerId) {
supportPeers[messageId.peerId] = peer
}
} else {
if let message = transaction.getMessage(messageId) {
messages.append(message)
} else {
missingMessageIds.insert(messageId)
if let peer = transaction.getPeer(messageId.peerId) {
supportPeers[messageId.peerId] = peer
}
}
}
}
return (messages, missingMessageIds, supportPeers)
}
if case .cloud = strategy {
return postboxSignal
|> castError(GetMessagesError.self)
|> mapToSignal { (existMessages, missingMessageIds, supportPeers) in
var signals: [Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), GetMessagesError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(missingMessageIds) {
if let peer = supportPeers[peerId] {
var signal: Signal<Api.messages.Messages, MTRpcError>?
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup {
signal = network.request(Api.functions.messages.getMessages(id: messageIds.map({ Api.InputMessage.inputMessageID(id: $0.id) })))
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
if let inputChannel = apiInputChannel(peer) {
signal = network.request(Api.functions.channels.getMessages(channel: inputChannel, id: messageIds.map({ Api.InputMessage.inputMessageID(id: $0.id) })))
}
}
if let signal = signal {
signals.append(signal |> map { result in
switch result {
case let .messages(messages, _, chats, users):
return (peer, messages, chats, users)
case let .messagesSlice(_, _, _, _, _, messages, _, chats, users):
return (peer, messages, chats, users)
case let .channelMessages(_, _, _, _, messages, apiTopics, chats, users):
let _ = apiTopics
return (peer, messages, chats, users)
case .messagesNotModified:
return (peer, [], [], [])
}
} |> `catch` { error in
if error.errorDescription == "CHANNEL_PRIVATE" {
return .fail(.privateChannel)
} else {
return Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), GetMessagesError>.single((peer, [], [], []))
}
})
}
}
}
return .single(.progress)
|> castError(GetMessagesError.self)
|> then(combineLatest(signals) |> mapToSignal { results -> Signal<GetMessagesResult, GetMessagesError> in
return postbox.transaction { transaction -> GetMessagesResult in
for (peer, messages, chats, users) in results {
if !messages.isEmpty {
var storeMessages: [StoreMessage] = []
for message in messages {
if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum) {
storeMessages.append(message)
}
}
_ = transaction.addMessages(storeMessages, location: .Random)
}
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
}
var loadedMessages:[Message] = []
for messageId in missingMessageIds {
if let message = transaction.getMessage(messageId) {
loadedMessages.append(message)
}
}
return .result(existMessages + loadedMessages)
}
|> castError(GetMessagesError.self)
})
}
} else {
return postboxSignal
|> castError(GetMessagesError.self)
|> map {
return .result($0.0)
}
}
}
@@ -0,0 +1,71 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
func _internal_markAllChatsAsRead(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return network.request(Api.functions.messages.getDialogUnreadMarks(flags: 0, parentPeer: nil))
|> map(Optional.init)
|> `catch` { _ -> Signal<[Api.DialogPeer]?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
guard let result = result else {
return .complete()
}
return postbox.transaction { transaction -> Signal<Void, NoError> in
var signals: [Signal<Void, NoError>] = []
for peer in result {
switch peer {
case let .dialogPeer(peer):
let peerId = peer.peerId
if peerId.namespace == Namespaces.Peer.CloudChannel {
if let inputChannel = transaction.getPeer(peerId).flatMap(apiInputChannel) {
signals.append(network.request(Api.functions.channels.readHistory(channel: inputChannel, maxId: Int32.max - 1))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
})
}
} else if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup {
if let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) {
signals.append(network.request(Api.functions.messages.readHistory(peer: inputPeer, maxId: Int32.max - 1))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AffectedMessages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .affectedMessages(pts, ptsCount):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
}
}
return .complete()
})
}
} else {
assertionFailure()
}
case .dialogPeerFolder:
assertionFailure()
}
}
let applyLocally = postbox.transaction { transaction -> Void in
}
return combineLatest(signals)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
|> then(applyLocally)
} |> switchToLatest
}
}
@@ -0,0 +1,252 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_markMessageContentAsConsumedInteractively(postbox: Postbox, messageId: MessageId) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if let message = transaction.getMessage(messageId), message.flags.contains(.Incoming) {
var updateMessage = false
var updatedAttributes = message.attributes
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? ConsumableContentMessageAttribute {
if !attribute.consumed {
updatedAttributes[i] = ConsumableContentMessageAttribute(consumed: true)
updateMessage = true
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
if let state = transaction.getPeerChatState(message.id.peerId) as? SecretChatState {
var layer: SecretChatLayer?
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
if let layer = layer {
var globallyUniqueIds: [Int64] = []
if let globallyUniqueId = message.globallyUniqueId {
globallyUniqueIds.append(globallyUniqueId)
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: message.id.peerId, operation: SecretChatOutgoingOperationContents.readMessagesContent(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: globallyUniqueIds), state: state)
if updatedState != state {
transaction.setPeerChatState(message.id.peerId, state: updatedState)
}
}
}
}
} else {
addSynchronizeConsumeMessageContentsOperation(transaction: transaction, messageIds: [message.id])
}
}
} else if let attribute = updatedAttributes[i] as? ConsumablePersonalMentionMessageAttribute, !attribute.consumed {
transaction.setPendingMessageAction(type: .consumeUnseenPersonalMessage, id: messageId, action: ConsumePersonalMessageAction())
updatedAttributes[i] = ConsumablePersonalMentionMessageAttribute(consumed: attribute.consumed, pending: true)
}
}
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? AutoremoveTimeoutMessageAttribute {
if attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0 {
var timeout = attribute.timeout
if let duration = message.secretMediaDuration {
timeout = max(timeout, Int32(duration))
}
updatedAttributes[i] = AutoremoveTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: timestamp)
updateMessage = true
if messageId.peerId.namespace == Namespaces.Peer.SecretChat {
var layer: SecretChatLayer?
let state = transaction.getPeerChatState(message.id.peerId) as? SecretChatState
if let state = state {
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
}
if let state = state, let layer = layer, let globallyUniqueId = message.globallyUniqueId {
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: messageId.peerId, operation: .readMessagesContent(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: [globallyUniqueId]), state: state)
if updatedState != state {
transaction.setPeerChatState(messageId.peerId, state: updatedState)
}
}
}
}
} else if let attribute = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute {
if attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0 {
var timeout = attribute.timeout
if let duration = message.secretMediaDuration, timeout != viewOnceTimeout {
timeout = max(timeout, Int32(duration))
}
updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: timestamp)
updateMessage = true
if messageId.peerId.namespace == Namespaces.Peer.SecretChat {
var layer: SecretChatLayer?
let state = transaction.getPeerChatState(message.id.peerId) as? SecretChatState
if let state = state {
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
}
if let state = state, let layer = layer, let globallyUniqueId = message.globallyUniqueId {
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: messageId.peerId, operation: .readMessagesContent(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: [globallyUniqueId]), state: state)
if updatedState != state {
transaction.setPeerChatState(messageId.peerId, state: updatedState)
}
}
}
}
}
}
if updateMessage {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
})
}
}
}
}
func _internal_markReactionsAsSeenInteractively(postbox: Postbox, messageId: MessageId) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if let message = transaction.getMessage(messageId), message.tags.contains(.unseenReaction) {
var updateMessage = false
var updatedAttributes = message.attributes
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? ReactionsMessageAttribute, attribute.hasUnseen {
updatedAttributes[i] = attribute.withAllSeen()
updateMessage = true
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
} else {
transaction.setPendingMessageAction(type: .readReaction, id: messageId, action: ReadReactionAction())
}
}
}
if updateMessage {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var tags = currentMessage.tags
tags.remove(.unseenReaction)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media))
})
}
}
}
}
func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: MessageId, consumeDate: Int32?) {
if let message = transaction.getMessage(messageId) {
var updateMessage = false
var updatedAttributes = message.attributes
var updatedMedia = message.media
var updatedTags = message.tags
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? ConsumableContentMessageAttribute {
if !attribute.consumed {
updatedAttributes[i] = ConsumableContentMessageAttribute(consumed: true)
updateMessage = true
}
} else if let attribute = updatedAttributes[i] as? ConsumablePersonalMentionMessageAttribute, !attribute.consumed {
if attribute.pending {
transaction.setPendingMessageAction(type: .consumeUnseenPersonalMessage, id: messageId, action: nil)
}
updatedAttributes[i] = ConsumablePersonalMentionMessageAttribute(consumed: true, pending: false)
updatedTags.remove(.unseenPersonalMessage)
updateMessage = true
}
}
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let countdownBeginTime = consumeDate ?? timestamp
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? AutoremoveTimeoutMessageAttribute {
if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && message.containsSecretMedia {
updatedAttributes[i] = AutoremoveTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: countdownBeginTime)
updateMessage = true
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
} else {
if attribute.timeout == viewOnceTimeout || timestamp >= countdownBeginTime + attribute.timeout {
for i in 0 ..< updatedMedia.count {
if let _ = updatedMedia[i] as? TelegramMediaImage {
updatedMedia[i] = TelegramMediaExpiredContent(data: .image)
} else if let file = updatedMedia[i] as? TelegramMediaFile {
if file.isInstantVideo {
updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage)
} else if file.isVoice {
updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage)
} else {
updatedMedia[i] = TelegramMediaExpiredContent(data: .file)
}
}
}
}
}
}
} else if let attribute = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute {
if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && message.containsSecretMedia {
updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: countdownBeginTime)
updateMessage = true
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
} else {
for i in 0 ..< updatedMedia.count {
if attribute.timeout == viewOnceTimeout || timestamp >= countdownBeginTime + attribute.timeout {
if let _ = updatedMedia[i] as? TelegramMediaImage {
updatedMedia[i] = TelegramMediaExpiredContent(data: .image)
} else if let file = updatedMedia[i] as? TelegramMediaFile {
if file.isInstantVideo {
updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage)
} else if file.isVoice {
updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage)
} else {
updatedMedia[i] = TelegramMediaExpiredContent(data: .file)
}
}
}
}
}
}
}
}
if updateMessage {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: updatedMedia))
})
}
}
}
@@ -0,0 +1,160 @@
import Postbox
public enum EngineMedia: Equatable {
public typealias Id = MediaId
case image(TelegramMediaImage)
case file(TelegramMediaFile)
case geo(TelegramMediaMap)
case contact(TelegramMediaContact)
case action(TelegramMediaAction)
case dice(TelegramMediaDice)
case expiredContent(TelegramMediaExpiredContent)
case game(TelegramMediaGame)
case invoice(TelegramMediaInvoice)
case poll(TelegramMediaPoll)
case unsupported(TelegramMediaUnsupported)
case webFile(TelegramMediaWebFile)
case webpage(TelegramMediaWebpage)
case story(TelegramMediaStory)
case giveaway(TelegramMediaGiveaway)
case giveawayResults(TelegramMediaGiveawayResults)
case paidContent(TelegramMediaPaidContent)
case todo(TelegramMediaTodo)
case liveStream(TelegramMediaLiveStream)
}
public extension EngineMedia {
var id: Id? {
switch self {
case let .image(image):
return image.id
case let .file(file):
return file.id
case let .geo(geo):
return geo.id
case let .contact(contact):
return contact.id
case let .action(action):
return action.id
case let .dice(dice):
return dice.id
case let .expiredContent(expiredContent):
return expiredContent.id
case let .game(game):
return game.id
case let .invoice(invoice):
return invoice.id
case let .poll(poll):
return poll.id
case let .unsupported(unsupported):
return unsupported.id
case let .webFile(webFile):
return webFile.id
case let .webpage(webpage):
return webpage.id
case let .story(story):
return story.id
case let .giveaway(giveaway):
return giveaway.id
case let .giveawayResults(giveawayResults):
return giveawayResults.id
case let .paidContent(paidContent):
return paidContent.id
case .todo:
return nil
case .liveStream:
return nil
}
}
}
public extension EngineMedia {
init(_ media: Media) {
switch media {
case let image as TelegramMediaImage:
self = .image(image)
case let file as TelegramMediaFile:
self = .file(file)
case let geo as TelegramMediaMap:
self = .geo(geo)
case let contact as TelegramMediaContact:
self = .contact(contact)
case let action as TelegramMediaAction:
self = .action(action)
case let dice as TelegramMediaDice:
self = .dice(dice)
case let expiredContent as TelegramMediaExpiredContent:
self = .expiredContent(expiredContent)
case let game as TelegramMediaGame:
self = .game(game)
case let invoice as TelegramMediaInvoice:
self = .invoice(invoice)
case let poll as TelegramMediaPoll:
self = .poll(poll)
case let unsupported as TelegramMediaUnsupported:
self = .unsupported(unsupported)
case let webFile as TelegramMediaWebFile:
self = .webFile(webFile)
case let webpage as TelegramMediaWebpage:
self = .webpage(webpage)
case let story as TelegramMediaStory:
self = .story(story)
case let giveaway as TelegramMediaGiveaway:
self = .giveaway(giveaway)
case let giveawayResults as TelegramMediaGiveawayResults:
self = .giveawayResults(giveawayResults)
case let paidContent as TelegramMediaPaidContent:
self = .paidContent(paidContent)
case let todo as TelegramMediaTodo:
self = .todo(todo)
case let liveStream as TelegramMediaLiveStream:
self = .liveStream(liveStream)
default:
preconditionFailure()
}
}
func _asMedia() -> Media {
switch self {
case let .image(image):
return image
case let .file(file):
return file
case let .geo(geo):
return geo
case let .contact(contact):
return contact
case let .action(action):
return action
case let .dice(dice):
return dice
case let .expiredContent(expiredContent):
return expiredContent
case let .game(game):
return game
case let .invoice(invoice):
return invoice
case let .poll(poll):
return poll
case let .unsupported(unsupported):
return unsupported
case let .webFile(webFile):
return webFile
case let .webpage(webpage):
return webpage
case let .story(story):
return story
case let .giveaway(giveaway):
return giveaway
case let .giveawayResults(giveawayResults):
return giveawayResults
case let .paidContent(paidContent):
return paidContent
case let .todo(todo):
return todo
case let .liveStream(liveStream):
return liveStream
}
}
}
@@ -0,0 +1,274 @@
import Foundation
import Postbox
public enum MediaArea: Codable, Equatable {
private enum CodingKeys: CodingKey {
case type
case coordinates
case value
case flags
case temperature
case color
}
public struct Coordinates: Codable, Equatable {
private enum CodingKeys: CodingKey {
case x
case y
case width
case height
case rotation
case cornerRadius
}
public var x: Double
public var y: Double
public var width: Double
public var height: Double
public var rotation: Double
public var cornerRadius: Double?
public init(
x: Double,
y: Double,
width: Double,
height: Double,
rotation: Double,
cornerRadius: Double?
) {
self.x = x
self.y = y
self.width = width
self.height = height
self.rotation = rotation
self.cornerRadius = cornerRadius
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.x = try container.decode(Double.self, forKey: .x)
self.y = try container.decode(Double.self, forKey: .y)
self.width = try container.decode(Double.self, forKey: .width)
self.height = try container.decode(Double.self, forKey: .height)
self.rotation = try container.decode(Double.self, forKey: .rotation)
self.cornerRadius = try container.decodeIfPresent(Double.self, forKey: .cornerRadius)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.x, forKey: .x)
try container.encode(self.y, forKey: .y)
try container.encode(self.width, forKey: .width)
try container.encode(self.height, forKey: .height)
try container.encode(self.rotation, forKey: .rotation)
try container.encodeIfPresent(self.cornerRadius, forKey: .cornerRadius)
}
}
public struct Venue: Codable, Equatable {
private enum CodingKeys: CodingKey {
case latitude
case longitude
case venue
case address
case queryId
case resultId
}
public let latitude: Double
public let longitude: Double
public let venue: MapVenue?
public let address: MapGeoAddress?
public let queryId: Int64?
public let resultId: String?
public init(
latitude: Double,
longitude: Double,
venue: MapVenue?,
address: MapGeoAddress?,
queryId: Int64?,
resultId: String?
) {
self.latitude = latitude
self.longitude = longitude
self.venue = venue
self.address = address
self.queryId = queryId
self.resultId = resultId
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.latitude = try container.decode(Double.self, forKey: .latitude)
self.longitude = try container.decode(Double.self, forKey: .longitude)
if let venueData = try container.decodeIfPresent(Data.self, forKey: .venue) {
self.venue = PostboxDecoder(buffer: MemoryBuffer(data: venueData)).decodeRootObject() as? MapVenue
} else {
self.venue = nil
}
if let addressData = try container.decodeIfPresent(Data.self, forKey: .address) {
self.address = PostboxDecoder(buffer: MemoryBuffer(data: addressData)).decodeRootObject() as? MapGeoAddress
} else {
self.address = nil
}
self.queryId = try container.decodeIfPresent(Int64.self, forKey: .queryId)
self.resultId = try container.decodeIfPresent(String.self, forKey: .resultId)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.latitude, forKey: .latitude)
try container.encode(self.longitude, forKey: .longitude)
if let venue = self.venue {
let encoder = PostboxEncoder()
encoder.encodeRootObject(venue)
let venueData = encoder.makeData()
try container.encode(venueData, forKey: .venue)
}
if let address = self.address {
let encoder = PostboxEncoder()
encoder.encodeRootObject(address)
let addressData = encoder.makeData()
try container.encode(addressData, forKey: .address)
}
try container.encodeIfPresent(self.queryId, forKey: .queryId)
try container.encodeIfPresent(self.resultId, forKey: .resultId)
}
}
case venue(coordinates: Coordinates, venue: Venue)
case reaction(coordinates: Coordinates, reaction: MessageReaction.Reaction, flags: ReactionFlags)
case channelMessage(coordinates: Coordinates, messageId: EngineMessage.Id)
case link(coordinates: Coordinates, url: String)
case weather(coordinates: Coordinates, emoji: String, temperature: Double, color: Int32)
case starGift(coordinates: Coordinates, slug: String)
public struct ReactionFlags: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let isDark = ReactionFlags(rawValue: 1 << 0)
public static let isFlipped = ReactionFlags(rawValue: 1 << 1)
}
private enum MediaAreaType: Int32 {
case venue
case reaction
case channelMessage
case link
case weather
case starGift
}
public enum DecodingError: Error {
case generic
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let type = MediaAreaType(rawValue: try container.decode(Int32.self, forKey: .type)) else {
throw DecodingError.generic
}
switch type {
case .venue:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let venue = try container.decode(MediaArea.Venue.self, forKey: .value)
self = .venue(coordinates: coordinates, venue: venue)
case .reaction:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let reaction = try container.decode(MessageReaction.Reaction.self, forKey: .value)
let flags = ReactionFlags(rawValue: try container.decodeIfPresent(Int32.self, forKey: .flags) ?? 0)
self = .reaction(coordinates: coordinates, reaction: reaction, flags: flags)
case .channelMessage:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let messageId = try container.decode(MessageId.self, forKey: .value)
self = .channelMessage(coordinates: coordinates, messageId: messageId)
case .link:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let url = try container.decode(String.self, forKey: .value)
self = .link(coordinates: coordinates, url: url)
case .weather:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let emoji = try container.decode(String.self, forKey: .value)
let temperature = try container.decode(Double.self, forKey: .temperature)
let color = try container.decodeIfPresent(Int32.self, forKey: .color) ?? 0
self = .weather(coordinates: coordinates, emoji: emoji, temperature: temperature, color: color)
case .starGift:
let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates)
let slug = try container.decode(String.self, forKey: .value)
self = .starGift(coordinates: coordinates, slug: slug)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .venue(coordinates, venue):
try container.encode(MediaAreaType.venue.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(venue, forKey: .value)
case let .reaction(coordinates, reaction, flags):
try container.encode(MediaAreaType.reaction.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(reaction, forKey: .value)
try container.encode(flags.rawValue, forKey: .flags)
case let .channelMessage(coordinates, messageId):
try container.encode(MediaAreaType.channelMessage.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(messageId, forKey: .value)
case let .link(coordinates, url):
try container.encode(MediaAreaType.link.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(url, forKey: .value)
case let .weather(coordinates, emoji, temperature, color):
try container.encode(MediaAreaType.weather.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(emoji, forKey: .value)
try container.encode(temperature, forKey: .temperature)
try container.encode(color, forKey: .color)
case let .starGift(coordinates, slug):
try container.encode(MediaAreaType.starGift.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(slug, forKey: .value)
}
}
}
public extension MediaArea {
var coordinates: Coordinates {
switch self {
case let .venue(coordinates, _):
return coordinates
case let .reaction(coordinates, _, _):
return coordinates
case let .channelMessage(coordinates, _):
return coordinates
case let .link(coordinates, _):
return coordinates
case let .weather(coordinates, _, _, _):
return coordinates
case let .starGift(coordinates, _):
return coordinates
}
}
}
@@ -0,0 +1,230 @@
import Postbox
public final class EngineMessage: Equatable {
public static let newTopicThreadId: Int64 = Message.newTopicThreadId
public typealias Id = MessageId
public typealias StableId = UInt32
public typealias Index = MessageIndex
public typealias Tags = MessageTags
public typealias Attribute = MessageAttribute
public typealias GroupInfo = MessageGroupInfo
public typealias Flags = MessageFlags
public typealias GlobalTags = GlobalMessageTags
public typealias LocalTags = LocalMessageTags
public typealias ForwardInfo = MessageForwardInfo
public typealias CustomTag = MemoryBuffer
public enum InputTag: Hashable {
case tag(Tags)
case custom(CustomTag)
}
private let impl: Message
public var stableId: UInt32 {
return self.impl.stableId
}
public var stableVersion: UInt32 {
return self.impl.stableVersion
}
public var id: Id {
return self.impl.id
}
public var globallyUniqueId: Int64? {
return self.impl.globallyUniqueId
}
public var groupingKey: Int64? {
return self.impl.groupingKey
}
public var groupInfo: GroupInfo? {
return self.impl.groupInfo
}
public var threadId: Int64? {
return self.impl.threadId
}
public var timestamp: Int32 {
return self.impl.timestamp
}
public var flags: Flags {
return self.impl.flags
}
public var tags: Tags {
return self.impl.tags
}
public var globalTags: GlobalTags {
return self.impl.globalTags
}
public var localTags: LocalTags {
return self.impl.localTags
}
public var customTags: [CustomTag] {
return self.impl.customTags
}
public var forwardInfo: ForwardInfo? {
return self.impl.forwardInfo
}
public var author: EnginePeer? {
return self.impl.author.flatMap(EnginePeer.init)
}
public var text: String {
return self.impl.text
}
public var attributes: [Attribute] {
return self.impl.attributes
}
public var media: [Media] {
return self.impl.media
}
public var peers: SimpleDictionary<EnginePeer.Id, Peer> {
return self.impl.peers
}
public var associatedMessages: SimpleDictionary<EngineMessage.Id, Message> {
return self.impl.associatedMessages
}
public var associatedMessageIds: [EngineMessage.Id] {
return self.impl.associatedMessageIds
}
public var associatedMedia: [MediaId: Media] {
return self.impl.associatedMedia
}
public var associatedThreadInfo: Message.AssociatedThreadInfo? {
return self.impl.associatedThreadInfo
}
public var associatedStories: [StoryId: CodableEntry] {
return self.impl.associatedStories
}
public var index: MessageIndex {
return self.impl.index
}
public init(
stableId: UInt32,
stableVersion: UInt32,
id: EngineMessage.Id,
globallyUniqueId: Int64?,
groupingKey: Int64?,
groupInfo: EngineMessage.GroupInfo?,
threadId: Int64?,
timestamp: Int32,
flags: EngineMessage.Flags,
tags: EngineMessage.Tags,
globalTags: EngineMessage.GlobalTags,
localTags: EngineMessage.LocalTags,
customTags: [EngineMessage.CustomTag],
forwardInfo: EngineMessage.ForwardInfo?,
author: EnginePeer?,
text: String,
attributes: [Attribute],
media: [EngineMedia],
peers: [EnginePeer.Id: EnginePeer],
associatedMessages: [EngineMessage.Id: EngineMessage],
associatedMessageIds: [EngineMessage.Id],
associatedMedia: [MediaId: Media],
associatedThreadInfo: Message.AssociatedThreadInfo?,
associatedStories: [StoryId: CodableEntry]
) {
var mappedPeers: [PeerId: Peer] = [:]
for (id, peer) in peers {
mappedPeers[id] = peer._asPeer()
}
var mappedAssociatedMessages: [MessageId: Message] = [:]
for (id, message) in associatedMessages {
mappedAssociatedMessages[id] = message._asMessage()
}
self.impl = Message(
stableId: stableId,
stableVersion: stableVersion,
id: id,
globallyUniqueId: globallyUniqueId,
groupingKey: groupingKey,
groupInfo: groupInfo,
threadId: threadId,
timestamp: timestamp,
flags: flags,
tags: tags,
globalTags: globalTags,
localTags: localTags,
customTags: customTags,
forwardInfo: forwardInfo,
author: author?._asPeer(),
text: text,
attributes: attributes,
media: media.map { $0._asMedia() },
peers: SimpleDictionary(mappedPeers),
associatedMessages: SimpleDictionary(mappedAssociatedMessages),
associatedMessageIds: associatedMessageIds,
associatedMedia: associatedMedia,
associatedThreadInfo: associatedThreadInfo,
associatedStories: associatedStories
)
}
public init(_ impl: Message) {
self.impl = impl
}
public func _asMessage() -> Message {
return self.impl
}
public static func ==(lhs: EngineMessage, rhs: EngineMessage) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.globallyUniqueId != rhs.globallyUniqueId {
return false
}
if lhs.groupingKey != rhs.groupingKey {
return false
}
if lhs.groupInfo != rhs.groupInfo {
return false
}
if lhs.threadId != rhs.threadId {
return false
}
if lhs.timestamp != rhs.timestamp {
return false
}
if lhs.flags != rhs.flags {
return false
}
if lhs.tags != rhs.tags {
return false
}
if lhs.globalTags != rhs.globalTags {
return false
}
if lhs.localTags != rhs.localTags {
return false
}
if lhs.forwardInfo != rhs.forwardInfo {
return false
}
if lhs.author != rhs.author {
return false
}
if lhs.text != rhs.text {
return false
}
if !areMediaArraysEqual(lhs.media, rhs.media) {
return false
}
if lhs.associatedThreadInfo != rhs.associatedThreadInfo {
return false
}
if lhs.attributes.count != rhs.attributes.count {
return false
}
if lhs.stableVersion != rhs.stableVersion {
return false
}
return true
}
}
@@ -0,0 +1,120 @@
import Postbox
import SwiftSignalKit
import TelegramApi
public final class MessageReadStats {
public let reactionCount: Int
public let peers: [EnginePeer]
public let readTimestamps: [EnginePeer.Id: Int32]
public init(reactionCount: Int, peers: [EnginePeer], readTimestamps: [EnginePeer.Id: Int32]) {
self.reactionCount = reactionCount
self.peers = peers
self.readTimestamps = readTimestamps
}
}
func _internal_messageReadStats(account: Account, id: MessageId) -> Signal<MessageReadStats?, NoError> {
return account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(id.peerId)
}
|> mapToSignal { peer -> Signal<MessageReadStats?, NoError> in
guard let peer, let inputPeer = apiInputPeer(peer) else {
return .single(nil)
}
if id.namespace != Namespaces.Message.Cloud {
return .single(nil)
}
if id.peerId.namespace == Namespaces.Peer.CloudUser {
return account.network.request(Api.functions.messages.getOutboxReadDate(peer: inputPeer, msgId: id.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.OutboxReadDate?, NoError> in
return .single(nil)
}
|> map { result -> MessageReadStats? in
guard let result else {
return MessageReadStats(reactionCount: 0, peers: [], readTimestamps: [:])
}
switch result {
case let .outboxReadDate(date):
return MessageReadStats(reactionCount: 0, peers: [EnginePeer(peer)], readTimestamps: [peer.id: date])
}
}
} else {
let readPeers: Signal<[(Int64, Int32)]?, NoError> = account.network.request(Api.functions.messages.getMessageReadParticipants(peer: inputPeer, msgId: id.id))
|> map { result -> [(Int64, Int32)]? in
var items: [(Int64, Int32)] = []
for item in result {
switch item {
case let .readParticipantDate(userId, date):
items.append((userId, date))
}
}
return items
}
|> `catch` { _ -> Signal<[(Int64, Int32)]?, NoError> in
return .single(nil)
}
let reactionCount: Signal<Int, NoError> = account.network.request(Api.functions.messages.getMessageReactionsList(flags: 0, peer: inputPeer, id: id.id, reaction: nil, offset: nil, limit: 1))
|> map { result -> Int in
switch result {
case let .messageReactionsList(_, count, _, _, _, _):
return Int(count)
}
}
|> `catch` { _ -> Signal<Int, NoError> in
return .single(0)
}
return combineLatest(readPeers, reactionCount)
|> mapToSignal { result, reactionCount -> Signal<MessageReadStats?, NoError> in
return account.postbox.transaction { transaction -> (peerIds: [PeerId], readTimestamps: [PeerId: Int32], missingPeerIds: [PeerId]) in
var peerIds: [PeerId] = []
var readTimestamps: [PeerId: Int32] = [:]
var missingPeerIds: [PeerId] = []
let authorId = transaction.getMessage(id)?.author?.id
if let result = result {
for (id, timestamp) in result {
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
readTimestamps[peerId] = timestamp
if peerId == account.peerId {
continue
}
if peerId == authorId {
continue
}
peerIds.append(peerId)
if transaction.getPeer(peerId) == nil {
missingPeerIds.append(peerId)
}
}
}
return (peerIds: peerIds, readTimestamps: readTimestamps, missingPeerIds: missingPeerIds)
}
|> mapToSignal { peerIds, readTimestamps, missingPeerIds -> Signal<MessageReadStats?, NoError> in
if missingPeerIds.isEmpty || id.peerId.namespace != Namespaces.Peer.CloudChannel {
return account.postbox.transaction { transaction -> MessageReadStats? in
return MessageReadStats(reactionCount: reactionCount, peers: peerIds.compactMap { peerId -> EnginePeer? in
return transaction.getPeer(peerId).flatMap(EnginePeer.init)
}, readTimestamps: readTimestamps)
}
} else {
return _internal_channelMembers(postbox: account.postbox, network: account.network, accountPeerId: account.peerId, peerId: id.peerId, category: .recent(.all), offset: 0, limit: 50, hash: 0)
|> mapToSignal { _ -> Signal<MessageReadStats?, NoError> in
return account.postbox.transaction { transaction -> MessageReadStats? in
return MessageReadStats(reactionCount: reactionCount, peers: peerIds.compactMap { peerId -> EnginePeer? in
return transaction.getPeer(peerId).flatMap(EnginePeer.init)
}, readTimestamps: readTimestamps)
}
}
}
}
}
}
}
}
@@ -0,0 +1,189 @@
import Foundation
import Postbox
import SwiftSignalKit
func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> Bool {
guard let message = _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: postpone, correlationId: correlationId) else {
return false
}
let _ = enqueueMessages(account: account, peerId: peerId, messages: [message]).start()
return true
}
func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> EnqueueMessage? {
var replyToMessageId = replyToMessageId
if replyToMessageId == nil, let threadId = threadId {
replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(clamping: threadId)), quote: nil, todoItemId: nil)
}
var webpageUrl: String?
if case let .webpage(_, _, url, _, _) = result.message {
webpageUrl = url
}
var attributes: [MessageAttribute] = []
attributes.append(OutgoingChatContextResultMessageAttribute(queryId: result.queryId, id: result.id, hideVia: hideVia, webpageUrl: webpageUrl))
if !hideVia {
attributes.append(InlineBotMessageAttribute(peerId: botId, title: nil))
}
if let scheduleTime = scheduleTime {
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime, repeatPeriod: nil))
}
if silentPosting {
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
}
if let sendPaidMessageStars {
attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: postpone))
}
switch result.message {
case let .auto(caption, entities, replyMarkup):
if let entities = entities {
attributes.append(entities)
}
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
switch result {
case let .internalReference(internalReference):
if internalReference.type == "game" {
if peerId.namespace == Namespaces.Peer.SecretChat {
let filteredAttributes = attributes.filter { attribute in
if let _ = attribute as? ReplyMarkupMessageAttribute {
return false
}
return true
}
if let media: Media = internalReference.file ?? internalReference.image {
return .message(text: caption, attributes: filteredAttributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else {
return .message(text: caption, attributes: filteredAttributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
}
} else {
return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaGame(gameId: 0, accessHash: 0, name: "", title: internalReference.title ?? "", description: internalReference.description ?? "", image: internalReference.image, file: internalReference.file)), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
}
} else if let file = internalReference.file, internalReference.type == "gif" {
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else if let image = internalReference.image {
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: image), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else if let file = internalReference.file {
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else {
return nil
}
case let .externalReference(externalReference):
if externalReference.type == "photo" {
if let thumbnail = externalReference.thumbnail {
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let thumbnailResource = thumbnail.resource
let imageDimensions = thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128)
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: tmpImage), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else {
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
}
} else if externalReference.type == "document" || externalReference.type == "gif" || externalReference.type == "audio" || externalReference.type == "voice" {
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if let thumbnail = externalReference.thumbnail {
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let thumbnailResource = thumbnail.resource
if thumbnail.mimeType.hasPrefix("video/") {
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource))
} else {
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
}
}
var fileName = "file"
if let content = externalReference.content {
var contentUrl: String?
if let resource = content.resource as? HttpReferenceMediaResource {
contentUrl = resource.url
} else if let resource = content.resource as? WebFileReferenceMediaResource {
contentUrl = resource.url
}
if let contentUrl = contentUrl, let url = URL(string: contentUrl) {
if !url.lastPathComponent.isEmpty {
fileName = url.lastPathComponent
}
}
}
var fileAttributes: [TelegramMediaFileAttribute] = []
fileAttributes.append(.FileName(fileName: fileName))
if externalReference.type == "gif" {
fileAttributes.append(.Animated)
}
if let dimensions = externalReference.content?.dimensions {
fileAttributes.append(.ImageSize(size: dimensions))
if externalReference.type == "gif" {
fileAttributes.append(.Video(duration: externalReference.content?.duration ?? 0.0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil))
}
}
if externalReference.type == "audio" || externalReference.type == "voice" {
fileAttributes.append(.Audio(isVoice: externalReference.type == "voice", duration: Int(Int32(externalReference.content?.duration ?? 0)), title: externalReference.title, performer: externalReference.description, waveform: nil))
}
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let resource: TelegramMediaResource
if peerId.namespace == Namespaces.Peer.SecretChat, let webResource = externalReference.content?.resource as? WebFileReferenceMediaResource {
resource = webResource
} else {
resource = EmptyMediaResource()
}
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: externalReference.content?.mimeType ?? "application/binary", size: nil, attributes: fileAttributes, alternativeRepresentations: [])
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: file), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
} else {
return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
}
}
case let .text(text, entities, disableUrlPreview, previewParameters, replyMarkup):
if let entities = entities {
attributes.append(entities)
}
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
if let previewParameters = previewParameters {
attributes.append(previewParameters)
}
if disableUrlPreview {
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
}
return .message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
case let .mapLocation(media, replyMarkup):
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
case let .contact(media, replyMarkup):
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
case let .invoice(media, replyMarkup):
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
return .message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
case let .webpage(text, entities, _, previewParameters, replyMarkup):
if let entities = entities {
attributes.append(entities)
}
if let replyMarkup = replyMarkup {
attributes.append(replyMarkup)
}
if let previewParameters = previewParameters {
attributes.append(previewParameters)
}
return .message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])
}
}
@@ -0,0 +1,32 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
func _internal_topPeerActiveLiveLocationMessages(viewTracker: AccountViewTracker, accountPeerId: PeerId, peerId: PeerId) -> Signal<(Peer?, [Message]), NoError> {
return viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 50, fixedCombinedReadStates: nil, tag: .tag(.liveLocation), orderStatistics: [], additionalData: [.peer(accountPeerId)])
|> map { (view, _, _) -> (Peer?, [Message]) in
var accountPeer: Peer?
for entry in view.additionalData {
if case let .peer(_, peer) = entry {
accountPeer = peer
break
}
}
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var result: [Message] = []
for entry in view.entries {
for media in entry.message.media {
if let location = media as? TelegramMediaMap, let liveBroadcastingTimeout = location.liveBroadcastingTimeout {
if liveBroadcastingTimeout == liveLocationIndefinitePeriod || entry.message.timestamp + liveBroadcastingTimeout > timestamp {
result.append(entry.message)
}
} else {
assertionFailure()
}
}
}
return (accountPeer, result)
}
}
@@ -0,0 +1,598 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
public extension Stories {
enum PendingTarget: Codable {
private enum CodingKeys: String, CodingKey {
case discriminator = "tt"
case peerId = "peerId"
case language = "language"
}
case myStories
case peer(PeerId)
case botPreview(id: PeerId, language: String?)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(Int32.self, forKey: .discriminator) {
case 0:
self = .myStories
case 1:
self = .peer(try container.decode(PeerId.self, forKey: .peerId))
case 2:
self = .botPreview(id: try container.decode(PeerId.self, forKey: .peerId), language: try container.decodeIfPresent(String.self, forKey: .language))
default:
self = .myStories
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .myStories:
try container.encode(0 as Int32, forKey: .discriminator)
case let .peer(peerId):
try container.encode(1 as Int32, forKey: .discriminator)
try container.encode(peerId, forKey: .peerId)
case let .botPreview(peerId, language):
try container.encode(2 as Int32, forKey: .discriminator)
try container.encode(peerId, forKey: .peerId)
try container.encodeIfPresent(language, forKey: .language)
}
}
}
struct PendingForwardInfo: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case peerId = "peerId"
case storyId = "storyId"
case isModified = "isModified"
}
public let peerId: EnginePeer.Id
public let storyId: Int32
public let isModified: Bool
public init(peerId: EnginePeer.Id, storyId: Int32, isModified: Bool) {
self.peerId = peerId
self.storyId = storyId
self.isModified = isModified
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.peerId = EnginePeer.Id(try container.decode(Int64.self, forKey: .peerId))
self.storyId = try container.decode(Int32.self, forKey: .storyId)
self.isModified = try container.decodeIfPresent(Bool.self, forKey: .isModified) ?? false
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.peerId.toInt64(), forKey: .peerId)
try container.encode(self.storyId, forKey: .storyId)
try container.encode(self.isModified, forKey: .isModified)
}
}
final class PendingItem: Equatable, Codable {
private enum CodingKeys: CodingKey {
case target
case stableId
case timestamp
case media
case mediaAreas
case text
case entities
case embeddedStickers
case pin
case privacy
case isForwardingDisabled
case period
case randomId
case forwardInfo
case uploadInfo
case folders
}
public let target: PendingTarget
public let stableId: Int32
public let timestamp: Int32
public let media: Media
public let mediaAreas: [MediaArea]
public let text: String
public let entities: [MessageTextEntity]
public let embeddedStickers: [TelegramMediaFile]
public let pin: Bool
public let privacy: EngineStoryPrivacy
public let isForwardingDisabled: Bool
public let period: Int32
public let randomId: Int64
public let forwardInfo: PendingForwardInfo?
public let folders: [Int64]
public let uploadInfo: StoryUploadInfo?
public init(
target: PendingTarget,
stableId: Int32,
timestamp: Int32,
media: Media,
mediaAreas: [MediaArea],
text: String,
entities: [MessageTextEntity],
embeddedStickers: [TelegramMediaFile],
pin: Bool,
privacy: EngineStoryPrivacy,
isForwardingDisabled: Bool,
period: Int32,
randomId: Int64,
forwardInfo: PendingForwardInfo?,
folders: [Int64],
uploadInfo: StoryUploadInfo?
) {
self.target = target
self.stableId = stableId
self.timestamp = timestamp
self.media = media
self.mediaAreas = mediaAreas
self.text = text
self.entities = entities
self.embeddedStickers = embeddedStickers
self.pin = pin
self.privacy = privacy
self.isForwardingDisabled = isForwardingDisabled
self.period = period
self.randomId = randomId
self.forwardInfo = forwardInfo
self.folders = folders
self.uploadInfo = uploadInfo
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.target = try container.decodeIfPresent(PendingTarget.self, forKey: .target) ?? .myStories
self.stableId = try container.decode(Int32.self, forKey: .stableId)
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
let mediaData = try container.decode(Data.self, forKey: .media)
self.media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as! Media
self.mediaAreas = try container.decodeIfPresent([MediaArea].self, forKey: .mediaAreas) ?? []
self.text = try container.decode(String.self, forKey: .text)
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
let stickersData = try container.decode(Data.self, forKey: .embeddedStickers)
let stickersDecoder = PostboxDecoder(buffer: MemoryBuffer(data: stickersData))
self.embeddedStickers = (try? stickersDecoder.decodeObjectArrayWithCustomDecoderForKey("stickers", decoder: { TelegramMediaFile(decoder: $0) })) ?? []
self.pin = try container.decode(Bool.self, forKey: .pin)
self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy)
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false
self.period = try container.decode(Int32.self, forKey: .period)
self.randomId = try container.decode(Int64.self, forKey: .randomId)
self.forwardInfo = try container.decodeIfPresent(PendingForwardInfo.self, forKey: .forwardInfo)
self.folders = try container.decodeIfPresent([Int64].self, forKey: .folders) ?? []
self.uploadInfo = try container.decodeIfPresent(StoryUploadInfo.self, forKey: .uploadInfo)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.target, forKey: .target)
try container.encode(self.stableId, forKey: .stableId)
try container.encode(self.timestamp, forKey: .timestamp)
let mediaEncoder = PostboxEncoder()
mediaEncoder.encodeRootObject(self.media)
try container.encode(mediaEncoder.makeData(), forKey: .media)
try container.encode(self.mediaAreas, forKey: .mediaAreas)
try container.encode(self.text, forKey: .text)
try container.encode(self.entities, forKey: .entities)
let stickersEncoder = PostboxEncoder()
stickersEncoder.encodeObjectArray(self.embeddedStickers, forKey: "stickers")
try container.encode(stickersEncoder.makeData(), forKey: .embeddedStickers)
try container.encode(self.pin, forKey: .pin)
try container.encode(self.privacy, forKey: .privacy)
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
try container.encode(self.period, forKey: .period)
try container.encode(self.randomId, forKey: .randomId)
try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo)
try container.encode(self.folders, forKey: .folders)
try container.encodeIfPresent(self.uploadInfo, forKey: .uploadInfo)
}
public static func ==(lhs: PendingItem, rhs: PendingItem) -> Bool {
if lhs.timestamp != rhs.timestamp {
return false
}
if lhs.stableId != rhs.stableId {
return false
}
if !lhs.media.isEqual(to: rhs.media) {
return false
}
if lhs.mediaAreas != rhs.mediaAreas {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.entities != rhs.entities {
return false
}
if lhs.pin != rhs.pin {
return false
}
if lhs.privacy != rhs.privacy {
return false
}
if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
return false
}
if lhs.period != rhs.period {
return false
}
if lhs.randomId != rhs.randomId {
return false
}
if lhs.forwardInfo != rhs.forwardInfo {
return false
}
if lhs.folders != rhs.folders {
return false
}
if lhs.uploadInfo != rhs.uploadInfo {
return false
}
return true
}
}
struct LocalState: Equatable, Codable {
public var items: [PendingItem]
public init(
items: [PendingItem]
) {
self.items = items
}
}
}
final class PendingStoryManager {
private final class PendingItemContext {
let queue: Queue
let item: Stories.PendingItem
let updated: () -> Void
var progress: Float = 0.0
var disposable: Disposable?
init(queue: Queue, item: Stories.PendingItem, updated: @escaping () -> Void) {
self.queue = queue
self.item = item
self.updated = updated
}
deinit {
self.disposable?.dispose()
}
}
private final class Impl {
let queue: Queue
let postbox: Postbox
let network: Network
let accountPeerId: PeerId
let stateManager: AccountStateManager
let messageMediaPreuploadManager: MessageMediaPreuploadManager
let revalidationContext: MediaReferenceRevalidationContext
let auxiliaryMethods: AccountAuxiliaryMethods
var itemsDisposable: Disposable?
var currentPendingItemContext: PendingItemContext?
var queuedPendingItems = Set<PeerId>()
var storyObserverContexts: [Int32: Bag<(Float) -> Void>] = [:]
private let allStoriesEventsPipe = ValuePipe<(Int32, Int32)>()
var allStoriesUploadEvents: Signal<(Int32, Int32), NoError> {
return self.allStoriesEventsPipe.signal()
}
private let allStoriesUploadProgressPromise = Promise<[PeerId: Float]>([:])
private var allStoriesUploadProgressValue: [PeerId: Float] = [:]
var allStoriesUploadProgress: Signal<[PeerId: Float], NoError> {
return self.allStoriesUploadProgressPromise.get()
}
private let hasPendingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
var hasPending: Signal<Bool, NoError> {
return self.hasPendingPromise.get()
}
func storyUploadProgress(stableId: Int32, next: @escaping (Float) -> Void) -> Disposable {
let bag: Bag<(Float) -> Void>
if let current = self.storyObserverContexts[stableId] {
bag = current
} else {
bag = Bag()
self.storyObserverContexts[stableId] = bag
}
let index = bag.add(next)
if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId {
next(currentPendingItemContext.progress)
} else {
next(0.0)
}
let queue = self.queue
return ActionDisposable { [weak self, weak bag] in
queue.async {
guard let `self` = self else {
return
}
if let bag = bag, let listBag = self.storyObserverContexts[stableId], listBag === bag {
bag.remove(index)
if bag.isEmpty {
self.storyObserverContexts.removeValue(forKey: stableId)
}
}
}
}
}
init(queue: Queue, postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) {
self.queue = queue
self.postbox = postbox
self.network = network
self.accountPeerId = accountPeerId
self.stateManager = stateManager
self.messageMediaPreuploadManager = messageMediaPreuploadManager
self.revalidationContext = revalidationContext
self.auxiliaryMethods = auxiliaryMethods
self.itemsDisposable = (postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .local)])
|> deliverOn(self.queue)).start(next: { [weak self] views in
guard let `self` = self else {
return
}
guard let view = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView else {
return
}
let localState: Stories.LocalState
if let value = view.value?.get(Stories.LocalState.self) {
localState = value
} else {
localState = Stories.LocalState(items: [])
}
self.update(localState: localState)
})
}
deinit {
self.itemsDisposable?.dispose()
}
private func update(localState: Stories.LocalState) {
if let currentPendingItemContext = self.currentPendingItemContext, !localState.items.contains(where: { $0.randomId == currentPendingItemContext.item.randomId }) {
self.currentPendingItemContext = nil
self.queue.after(0.1, {
let _ = currentPendingItemContext
print(currentPendingItemContext)
})
}
self.queuedPendingItems = Set(localState.items.compactMap { item -> PeerId? in
switch item.target {
case .myStories:
return self.accountPeerId
case let .peer(id):
return id
case .botPreview:
return nil
}
})
if self.currentPendingItemContext == nil, let firstItem = localState.items.first {
let queue = self.queue
let itemStableId = firstItem.stableId
let pendingItemContext = PendingItemContext(queue: queue, item: firstItem, updated: { [weak self] in
queue.async {
guard let `self` = self else {
return
}
self.processContextsUpdated()
if let pendingItemContext = self.currentPendingItemContext, pendingItemContext.item.stableId == itemStableId, let bag = self.storyObserverContexts[itemStableId] {
for f in bag.copyItems() {
f(pendingItemContext.progress)
}
}
}
})
self.currentPendingItemContext = pendingItemContext
let toPeerId: PeerId
var isBotPreview = false
var botPreviewLanguage: String?
switch firstItem.target {
case .myStories:
toPeerId = self.accountPeerId
case let .peer(peerId):
toPeerId = peerId
case let .botPreview(peerId, language):
toPeerId = peerId
botPreviewLanguage = language
isBotPreview = true
}
let stableId = firstItem.stableId
if isBotPreview {
pendingItemContext.disposable = (_internal_uploadBotPreviewImpl(
postbox: self.postbox,
network: self.network,
accountPeerId: self.accountPeerId,
stateManager: self.stateManager,
messageMediaPreuploadManager: self.messageMediaPreuploadManager,
revalidationContext: self.revalidationContext,
auxiliaryMethods: self.auxiliaryMethods,
toPeerId: toPeerId,
language: botPreviewLanguage,
stableId: stableId,
media: firstItem.media,
mediaAreas: firstItem.mediaAreas,
text: firstItem.text,
entities: firstItem.entities,
embeddedStickers: firstItem.embeddedStickers,
randomId: firstItem.randomId
)
|> deliverOn(self.queue)).start(next: { [weak self] event in
guard let self else {
return
}
switch event {
case let .progress(progress):
if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId {
currentPendingItemContext.progress = progress
currentPendingItemContext.updated()
}
case let .completed(id):
if let id = id {
self.allStoriesEventsPipe.putNext((stableId, id))
}
// wait for the local state to change via Postbox
break
}
})
} else {
if let uploadInfo = pendingItemContext.item.uploadInfo {
let partTotalProgress = 1.0 / Float(uploadInfo.total)
pendingItemContext.progress = Float(uploadInfo.index) * partTotalProgress
}
pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), folders: firstItem.folders, randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo)
|> deliverOn(self.queue)).start(next: { [weak self] event in
guard let `self` = self else {
return
}
switch event {
case let .progress(progress):
if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId {
if let uploadInfo = currentPendingItemContext.item.uploadInfo {
let partTotalProgress = 1.0 / Float(uploadInfo.total)
currentPendingItemContext.progress = Float(uploadInfo.index) * partTotalProgress + progress * partTotalProgress
} else {
currentPendingItemContext.progress = progress
}
currentPendingItemContext.updated()
}
case let .completed(id):
if let id = id {
self.allStoriesEventsPipe.putNext((stableId, id))
}
// wait for the local state to change via Postbox
break
}
})
}
}
self.processContextsUpdated()
}
private func processContextsUpdated() {
var currentProgress: [PeerId: Float] = [:]
for peerId in self.queuedPendingItems {
currentProgress[peerId] = 0.0
}
if let currentPendingItemContext = self.currentPendingItemContext {
switch currentPendingItemContext.item.target {
case .myStories:
currentProgress[self.accountPeerId] = currentPendingItemContext.progress
case let .peer(id):
currentProgress[id] = currentPendingItemContext.progress
case .botPreview:
break
}
}
if self.allStoriesUploadProgressValue != currentProgress {
let previousProgress = self.allStoriesUploadProgressValue
self.allStoriesUploadProgressValue = currentProgress
if !previousProgress.isEmpty && currentProgress.isEmpty {
// Hack: the UI is updated after 2 Postbox queries
let signal: Signal<[PeerId: Float], NoError> = Signal { subscriber in
Postbox.sharedQueue.justDispatch {
Postbox.sharedQueue.justDispatch {
subscriber.putNext([:])
}
}
return EmptyDisposable
}
|> deliverOnMainQueue
self.allStoriesUploadProgressPromise.set(signal)
} else {
self.allStoriesUploadProgressPromise.set(.single(currentProgress))
}
}
self.hasPendingPromise.set(self.currentPendingItemContext != nil)
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
private let accountPeerId: PeerId
public var allStoriesUploadProgress: Signal<[PeerId: Float], NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.allStoriesUploadProgress.start(next: subscriber.putNext)
}
}
public var hasPending: Signal<Bool, NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.hasPending.start(next: subscriber.putNext)
}
}
public func storyUploadProgress(stableId: Int32) -> Signal<Float, NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.storyUploadProgress(stableId: stableId, next: subscriber.putNext)
}
}
public func allStoriesUploadEvents() -> Signal<(Int32, Int32), NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.allStoriesUploadEvents.start(next: subscriber.putNext)
}
}
init(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) {
let queue = Queue.mainQueue()
self.queue = queue
self.accountPeerId = accountPeerId
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods)
})
}
func lookUpPendingStoryIdMapping(peerId: PeerId, stableId: Int32) -> Int32? {
return _internal_lookUpPendingStoryIdMapping(peerId: peerId, stableId: stableId)
}
}
@@ -0,0 +1,436 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
public enum RequestMessageSelectPollOptionError {
case generic
}
func _internal_requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal<TelegramMediaPoll?, RequestMessageSelectPollOptionError> {
return account.postbox.loadedPeerWithId(messageId.peerId)
|> take(1)
|> castError(RequestMessageSelectPollOptionError.self)
|> mapToSignal { peer in
if let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.sendVote(peer: inputPeer, msgId: messageId.id, options: opaqueIdentifiers.map { Buffer(data: $0) }))
|> mapError { _ -> RequestMessageSelectPollOptionError in
return .generic
}
|> mapToSignal { result -> Signal<TelegramMediaPoll?, RequestMessageSelectPollOptionError> in
return account.postbox.transaction { transaction -> TelegramMediaPoll? in
var resultPoll: TelegramMediaPoll?
switch result {
case let .updates(updates, _, _, _, _):
for update in updates {
switch update {
case let .updateMessagePoll(_, id, poll, results):
let pollId = MediaId(namespace: Namespaces.Media.CloudPoll, id: id)
resultPoll = transaction.getMedia(pollId) as? TelegramMediaPoll
if let poll = poll {
switch poll {
case let .poll(_, flags, question, answers, closePeriod, _):
let publicity: TelegramMediaPollPublicity
if (flags & (1 << 1)) != 0 {
publicity = .public
} else {
publicity = .anonymous
}
let kind: TelegramMediaPollKind
if (flags & (1 << 3)) != 0 {
kind = .quiz
} else {
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
}
let questionText: String
let questionEntities: [MessageTextEntity]
switch question {
case let .textWithEntities(text, entities):
questionText = text
questionEntities = messageTextEntitiesFromApiEntities(entities)
}
resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod)
}
}
let resultsMin: Bool
switch results {
case let .pollResults(flags, _, _, _, _, _):
resultsMin = (flags & (1 << 0)) != 0
}
resultPoll = resultPoll?.withUpdatedResults(TelegramMediaPollResults(apiResults: results), min: resultsMin)
if let resultPoll = resultPoll {
updateMessageMedia(transaction: transaction, id: pollId, media: resultPoll)
}
default:
break
}
}
break
default:
break
}
account.stateManager.addUpdates(result)
return resultPoll
}
|> castError(RequestMessageSelectPollOptionError.self)
}
} else {
return .single(nil)
}
}
}
func _internal_requestClosePoll(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> (TelegramMediaPoll, Api.InputPeer)? in
guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else {
return nil
}
guard let message = transaction.getMessage(messageId) else {
return nil
}
for media in message.media {
if let poll = media as? TelegramMediaPoll {
return (poll, inputPeer)
}
}
return nil
}
|> mapToSignal { pollAndInputPeer -> Signal<Void, NoError> in
guard let (poll, inputPeer) = pollAndInputPeer, poll.pollId.namespace == Namespaces.Media.CloudPoll else {
return .complete()
}
var flags: Int32 = 0
flags |= 1 << 14
var pollFlags: Int32 = 0
switch poll.kind {
case let .poll(multipleAnswers):
if multipleAnswers {
pollFlags |= 1 << 2
}
case .quiz:
pollFlags |= 1 << 3
}
switch poll.publicity {
case .anonymous:
break
case .public:
pollFlags |= 1 << 1
}
var pollMediaFlags: Int32 = 0
var correctAnswers: [Buffer]?
if let correctAnswersValue = poll.correctAnswers {
pollMediaFlags |= 1 << 0
correctAnswers = correctAnswersValue.map { Buffer(data: $0) }
}
pollFlags |= 1 << 0
if poll.deadlineTimeout != nil {
pollFlags |= 1 << 4
}
var mappedSolution: String?
var mappedSolutionEntities: [Api.MessageEntity]?
if let solution = poll.results.solution {
mappedSolution = solution.text
mappedSolutionEntities = apiTextAttributeEntities(TextEntitiesMessageAttribute(entities: solution.entities), associatedPeers: SimpleDictionary())
pollMediaFlags |= 1 << 1
}
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: .textWithEntities(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary())), answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities), replyMarkup: nil, entities: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, quickReplyShortcutId: nil))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
stateManager.addUpdates(updates)
}
return .complete()
}
}
}
final class CachedPollOptionResult: Codable {
let peerIds: [PeerId]
let count: Int32
public static func key(pollId: MediaId, optionOpaqueIdentifier: Data) -> ValueBoxKey {
let key = ValueBoxKey(length: 4 + 8 + optionOpaqueIdentifier.count)
key.setInt32(0, value: pollId.namespace)
key.setInt64(4, value: pollId.id)
key.setData(4 + 8, value: optionOpaqueIdentifier)
return key
}
public init(peerIds: [PeerId], count: Int32) {
self.peerIds = peerIds
self.count = count
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.peerIds = (try container.decode([Int64].self, forKey: "peerIds")).map(PeerId.init)
self.count = try container.decode(Int32.self, forKey: "count")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "peerIds")
try container.encode(self.count, forKey: "count")
}
}
private final class PollResultsOptionContext {
private let queue: Queue
private let account: Account
private let pollId: MediaId
private let messageId: MessageId
private let opaqueIdentifier: Data
private let disposable = MetaDisposable()
private var isLoadingMore: Bool = false
private var hasLoadedOnce: Bool = false
private var canLoadMore: Bool = true
private var nextOffset: String?
private var results: [RenderedPeer] = []
private var count: Int
private var populateCache: Bool = true
let state = Promise<PollResultsOptionState>()
init(queue: Queue, account: Account, pollId: MediaId, messageId: MessageId, opaqueIdentifier: Data, count: Int) {
self.queue = queue
self.account = account
self.pollId = pollId
self.messageId = messageId
self.opaqueIdentifier = opaqueIdentifier
self.count = count
self.isLoadingMore = true
self.disposable.set((account.postbox.transaction { transaction -> (peers: [RenderedPeer], canLoadMore: Bool)? in
let cachedResult = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPollResults, key: CachedPollOptionResult.key(pollId: pollId, optionOpaqueIdentifier: opaqueIdentifier)))?.get(CachedPollOptionResult.self)
if let cachedResult = cachedResult, Int(cachedResult.count) == count {
var result: [RenderedPeer] = []
for peerId in cachedResult.peerIds {
if let peer = transaction.getPeer(peerId) {
result.append(RenderedPeer(peer: peer))
} else {
return nil
}
}
return (result, Int(cachedResult.count) > result.count)
} else {
return nil
}
}
|> deliverOn(self.queue)).start(next: { [weak self] cachedPeersAndCanLoadMore in
guard let strongSelf = self else {
return
}
strongSelf.isLoadingMore = false
if let (cachedPeers, canLoadMore) = cachedPeersAndCanLoadMore {
strongSelf.results = cachedPeers
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = canLoadMore
}
strongSelf.loadMore()
}))
}
deinit {
self.disposable.dispose()
}
func loadMore() {
if self.isLoadingMore {
return
}
self.isLoadingMore = true
let pollId = self.pollId
let messageId = self.messageId
let opaqueIdentifier = self.opaqueIdentifier
let account = self.account
let accountPeerId = account.peerId
let nextOffset = self.nextOffset
let populateCache = self.populateCache
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<([RenderedPeer], Int, String?), NoError> in
if let inputPeer = inputPeer {
var flags: Int32 = 1 << 0
if let _ = nextOffset {
flags |= (1 << 1)
}
let signal = account.network.request(Api.functions.messages.getPollVotes(flags: flags, peer: inputPeer, id: messageId.id, option: Buffer(data: opaqueIdentifier), offset: nextOffset, limit: nextOffset == nil ? 10 : 50))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.VotesList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<([RenderedPeer], Int, String?), NoError> in
return account.postbox.transaction { transaction -> ([RenderedPeer], Int, String?) in
guard let result = result else {
return ([], 0, nil)
}
switch result {
case let .votesList(_, count, votes, chats, users, nextOffset):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var resultPeers: [RenderedPeer] = []
for vote in votes {
let peerId: PeerId
switch vote {
case let .messagePeerVote(peerIdValue, _, _):
peerId = peerIdValue.peerId
case let .messagePeerVoteInputOption(peerIdValue, _):
peerId = peerIdValue.peerId
case let .messagePeerVoteMultiple(peerIdValue, _, _):
peerId = peerIdValue.peerId
}
if let peer = transaction.getPeer(peerId) {
resultPeers.append(RenderedPeer(peer: peer))
}
}
if populateCache {
if let entry = CodableEntry(CachedPollOptionResult(peerIds: resultPeers.map { $0.peerId }, count: count)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPollResults, key: CachedPollOptionResult.key(pollId: pollId, optionOpaqueIdentifier: opaqueIdentifier)), entry: entry)
}
}
return (resultPeers, Int(count), nextOffset)
}
}
}
return signal
} else {
return .single(([], 0, nil))
}
}
|> deliverOn(self.queue)).start(next: { [weak self] peers, updatedCount, nextOffset in
guard let strongSelf = self else {
return
}
if strongSelf.populateCache {
strongSelf.populateCache = false
strongSelf.results.removeAll()
}
var existingIds = Set(strongSelf.results.map { $0.peerId })
for peer in peers {
if !existingIds.contains(peer.peerId) {
strongSelf.results.append(peer)
existingIds.insert(peer.peerId)
}
}
strongSelf.isLoadingMore = false
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = nextOffset != nil
strongSelf.nextOffset = nextOffset
if strongSelf.canLoadMore {
strongSelf.count = max(updatedCount, strongSelf.results.count)
} else {
strongSelf.count = strongSelf.results.count
}
strongSelf.updateState()
}))
self.updateState()
}
func updateState() {
self.state.set(.single(PollResultsOptionState(peers: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count)))
}
}
public struct PollResultsOptionState: Equatable {
public var peers: [RenderedPeer]
public var isLoadingMore: Bool
public var hasLoadedOnce: Bool
public var canLoadMore: Bool
public var count: Int
}
public struct PollResultsState: Equatable {
public var options: [Data: PollResultsOptionState]
}
private final class PollResultsContextImpl {
private let queue: Queue
private var optionContexts: [Data: PollResultsOptionContext] = [:]
let state = Promise<PollResultsState>()
init(queue: Queue, account: Account, messageId: MessageId, poll: TelegramMediaPoll) {
self.queue = queue
for option in poll.options {
var count = 0
if let voters = poll.results.voters {
for voter in voters {
if voter.opaqueIdentifier == option.opaqueIdentifier {
count = Int(voter.count)
}
}
}
self.optionContexts[option.opaqueIdentifier] = PollResultsOptionContext(queue: self.queue, account: account, pollId: poll.pollId, messageId: messageId, opaqueIdentifier: option.opaqueIdentifier, count: count)
}
self.state.set(combineLatest(queue: self.queue, self.optionContexts.map { (opaqueIdentifier, context) -> Signal<(Data, PollResultsOptionState), NoError> in
return context.state.get()
|> map { state -> (Data, PollResultsOptionState) in
return (opaqueIdentifier, state)
}
})
|> map { states -> PollResultsState in
var options: [Data: PollResultsOptionState] = [:]
for (opaqueIdentifier, state) in states {
options[opaqueIdentifier] = state
}
return PollResultsState(options: options)
})
for (_, context) in self.optionContexts {
context.loadMore()
}
}
func loadMore(optionOpaqueIdentifier: Data) {
self.optionContexts[optionOpaqueIdentifier]?.loadMore()
}
}
public final class PollResultsContext {
private let queue: Queue = Queue()
private let impl: QueueLocalObject<PollResultsContextImpl>
public var state: Signal<PollResultsState, 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
}
}
init(account: Account, messageId: MessageId, poll: TelegramMediaPoll) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return PollResultsContextImpl(queue: queue, account: account, messageId: messageId, poll: poll)
})
}
public func loadMore(optionOpaqueIdentifier: Data) {
self.impl.with { impl in
impl.loadMore(optionOpaqueIdentifier: optionOpaqueIdentifier)
}
}
}
@@ -0,0 +1,70 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public struct PreparedInlineMessage: Equatable {
public let botId: EnginePeer.Id
public let queryId: Int64
public let result: ChatContextResult
public let peerTypes: ReplyMarkupButtonAction.PeerTypes
}
func _internal_getPreparedInlineMessage(account: Account, botId: EnginePeer.Id, id: String) -> Signal<PreparedInlineMessage?, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(botId).flatMap(apiInputUser)
}
|> mapToSignal { inputBot -> Signal<PreparedInlineMessage?, NoError> in
guard let inputBot else {
return .single(nil)
}
return account.network.request(Api.functions.messages.getPreparedInlineMessage(bot: inputBot, id: id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.PreparedInlineMessage?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<PreparedInlineMessage?, NoError> in
guard let result else {
return .single(nil)
}
return account.postbox.transaction { transaction -> PreparedInlineMessage? in
switch result {
case let .preparedInlineMessage(queryId, result, apiPeerTypes, cacheTime, users):
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: users))
let _ = cacheTime
return PreparedInlineMessage(
botId: botId,
queryId: queryId,
result: ChatContextResult(apiResult: result, queryId: queryId),
peerTypes: ReplyMarkupButtonAction.PeerTypes(apiType: apiPeerTypes)
)
}
}
}
}
}
func _internal_checkBotDownload(account: Account, botId: EnginePeer.Id, fileName: String, url: String) -> Signal<Bool, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(botId).flatMap(apiInputUser)
}
|> mapToSignal { inputBot -> Signal<Bool, NoError> in
guard let inputBot else {
return .single(false)
}
return account.network.request(Api.functions.bots.checkDownloadFileParams(bot: inputBot, fileName: fileName, url: url))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> map { value in
switch value {
case .boolTrue:
return true
case .boolFalse:
return false
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
import Foundation
import Postbox
import SwiftSignalKit
private struct RecentHashtagItemId {
public let rawValue: MemoryBuffer
var value: String {
return String(data: self.rawValue.makeData(), encoding: .utf8) ?? ""
}
init(_ rawValue: MemoryBuffer) {
self.rawValue = rawValue
}
init?(_ value: String) {
if let data = value.data(using: .utf8) {
self.rawValue = MemoryBuffer(data: data)
} else {
return nil
}
}
}
func addRecentlyUsedHashtag(transaction: Transaction, string: String) {
if let itemId = RecentHashtagItemId(string) {
if let entry = CodableEntry(RecentHashtagItem()) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlyUsedHashtags, item: OrderedItemListEntry(id: itemId.rawValue, contents: entry), removeTailIfCountExceeds: 100)
}
}
}
func _internal_removeRecentlyUsedHashtag(postbox: Postbox, string: String) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if let itemId = RecentHashtagItemId(string) {
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlyUsedHashtags, itemId: itemId.rawValue)
}
}
}
func _internal_recentlyUsedHashtags(postbox: Postbox) -> Signal<[String], NoError> {
return postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.RecentlyUsedHashtags)])
|> mapToSignal { view -> Signal<[String], NoError> in
return postbox.transaction { transaction -> [String] in
var result: [String] = []
if let view = view.views[.orderedItemList(id: Namespaces.OrderedItemList.RecentlyUsedHashtags)] as? OrderedItemListView {
for item in view.items {
let value = RecentHashtagItemId(item.id).value
result.append(value)
}
}
return result
}
}
}
@@ -0,0 +1,988 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
private struct DiscussionMessage {
var messageId: MessageId
var channelMessageId: MessageId?
var isChannelPost: Bool
var isForumPost: Bool
var isMonoforumPost: Bool
var maxMessage: MessageId?
var maxReadIncomingMessageId: MessageId?
var maxReadOutgoingMessageId: MessageId?
var unreadCount: Int
}
private class ReplyThreadHistoryContextImpl {
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let threadId: Int64
private var currentHole: (MessageHistoryHolesViewEntry, Disposable)?
struct State: Equatable {
var messageId: MessageId
var holeIndices: [MessageId.Namespace: IndexSet]
var maxReadIncomingMessageId: MessageId?
var maxReadOutgoingMessageId: MessageId?
}
let state = Promise<State>()
private var stateValue: State? {
didSet {
if let stateValue = self.stateValue {
if stateValue != oldValue {
self.state.set(.single(stateValue))
}
}
}
}
let maxReadOutgoingMessageId = Promise<MessageId?>()
private var maxReadOutgoingMessageIdValue: MessageId? {
didSet {
if self.maxReadOutgoingMessageIdValue != oldValue {
self.maxReadOutgoingMessageId.set(.single(self.maxReadOutgoingMessageIdValue))
}
}
}
private var maxReadIncomingMessageIdValue: MessageId?
let unreadCount = Promise<Int>()
private var unreadCountValue: Int = 0 {
didSet {
if self.unreadCountValue != oldValue {
self.unreadCount.set(.single(self.unreadCountValue))
}
}
}
private var initialStateDisposable: Disposable?
private var holesDisposable: Disposable?
private var readStateDisposable: Disposable?
private var updateInitialStateDisposable: Disposable?
private let readDisposable = MetaDisposable()
init(queue: Queue, account: Account, data: ChatReplyThreadMessage) {
self.queue = queue
self.account = account
self.peerId = data.peerId
self.threadId = data.threadId
let referencedMessageId = MessageId(peerId: data.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: data.threadId))
self.maxReadOutgoingMessageIdValue = data.maxReadOutgoingMessageId
self.maxReadOutgoingMessageId.set(.single(self.maxReadOutgoingMessageIdValue))
self.maxReadIncomingMessageIdValue = data.maxReadIncomingMessageId
self.unreadCountValue = data.unreadCount
self.unreadCount.set(.single(self.unreadCountValue))
self.initialStateDisposable = (account.postbox.transaction { transaction -> State in
var indices = transaction.getThreadIndexHoles(peerId: data.peerId, threadId: data.threadId, namespace: Namespaces.Message.Cloud)
indices.subtract(data.initialFilledHoles)
if let maxMessageId = data.maxMessage {
indices.remove(integersIn: Int(maxMessageId.id + 1) ..< Int(Int32.max))
} else {
indices.removeAll()
}
return State(messageId: referencedMessageId, holeIndices: [Namespaces.Message.Cloud: indices], maxReadIncomingMessageId: data.maxReadIncomingMessageId, maxReadOutgoingMessageId: data.maxReadOutgoingMessageId)
}
|> deliverOn(self.queue)).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
strongSelf.stateValue = state
strongSelf.state.set(.single(state))
})
let threadId = self.threadId
self.holesDisposable = (account.postbox.messageHistoryHolesView()
|> map { view -> MessageHistoryHolesViewEntry? in
for entry in view.entries {
switch entry.hole {
case let .peer(hole):
if hole.threadId == threadId {
return entry
}
}
}
return nil
}
|> distinctUntilChanged
|> deliverOn(self.queue)).start(next: { [weak self] entry in
guard let strongSelf = self else {
return
}
strongSelf.setCurrentHole(entry: entry)
})
self.readStateDisposable = (account.stateManager.threadReadStateUpdates
|> deliverOn(self.queue)).start(next: { [weak self] (_, outgoing) in
guard let strongSelf = self else {
return
}
if let value = outgoing[PeerAndBoundThreadId(peerId: referencedMessageId.peerId, threadId: Int64(referencedMessageId.id))] {
strongSelf.maxReadOutgoingMessageIdValue = MessageId(peerId: data.peerId, namespace: Namespaces.Message.Cloud, id: value)
}
})
let accountPeerId = account.peerId
let updateInitialState: Signal<DiscussionMessage, FetchChannelReplyThreadMessageError> = account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(data.peerId)
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { peer -> Signal<DiscussionMessage, FetchChannelReplyThreadMessageError> in
guard let peer = peer else {
return .fail(.generic)
}
guard let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
return account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: Int32(clamping: data.threadId)))
|> mapError { _ -> FetchChannelReplyThreadMessageError in
return .generic
}
|> mapToSignal { discussionMessage -> Signal<DiscussionMessage, FetchChannelReplyThreadMessageError> in
return account.postbox.transaction { transaction -> Signal<DiscussionMessage, FetchChannelReplyThreadMessageError> in
switch discussionMessage {
case let .discussionMessage(_, messages, maxId, readInboxMaxId, readOutboxMaxId, unreadCount, chats, users):
let parsedMessages = messages.compactMap { message -> StoreMessage? in
StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum)
}
guard let topMessage = parsedMessages.last, let parsedIndex = topMessage.index else {
return .fail(.generic)
}
var channelMessageId: MessageId?
var replyThreadAttribute: ReplyThreadMessageAttribute?
for attribute in topMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
channelMessageId = attribute.messageId
} else if let attribute = attribute as? ReplyThreadMessageAttribute {
replyThreadAttribute = attribute
}
}
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
let _ = transaction.addMessages(parsedMessages, location: .Random)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let resolvedMaxMessage: MessageId?
if let maxId = maxId {
resolvedMaxMessage = MessageId(
peerId: parsedIndex.id.peerId,
namespace: Namespaces.Message.Cloud,
id: maxId
)
} else {
resolvedMaxMessage = nil
}
var isChannelPost = false
for attribute in topMessage.attributes {
if let _ = attribute as? SourceReferenceMessageAttribute {
isChannelPost = true
break
}
}
let maxReadIncomingMessageId = readInboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
}
if let channelMessageId = channelMessageId, let replyThreadAttribute = replyThreadAttribute {
account.viewTracker.updateReplyInfoForMessageId(channelMessageId, info: AccountViewTracker.UpdatedMessageReplyInfo(
timestamp: Int32(CFAbsoluteTimeGetCurrent()),
commentsPeerId: parsedIndex.id.peerId,
maxReadIncomingMessageId: maxReadIncomingMessageId,
maxMessageId: resolvedMaxMessage
))
transaction.updateMessage(channelMessageId, update: { currentMessage in
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReplyThreadMessageAttribute {
attributes[j] = ReplyThreadMessageAttribute(
count: replyThreadAttribute.count,
latestUsers: attribute.latestUsers,
commentsPeerId: attribute.commentsPeerId,
maxMessageId: replyThreadAttribute.maxMessageId,
maxReadMessageId: replyThreadAttribute.maxReadMessageId
)
}
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
var isForumPost = false
var isMonoforumPost = false
if let channel = transaction.getPeer(parsedIndex.id.peerId) as? TelegramChannel {
if channel.isForumOrMonoForum {
isForumPost = true
}
if channel.isMonoForum {
isMonoforumPost = true
}
} else if let user = transaction.getPeer(parsedIndex.id.peerId) as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum) {
isForumPost = true
}
return .single(DiscussionMessage(
messageId: parsedIndex.id,
channelMessageId: channelMessageId,
isChannelPost: isChannelPost,
isForumPost: isForumPost,
isMonoforumPost: isMonoforumPost,
maxMessage: resolvedMaxMessage,
maxReadIncomingMessageId: maxReadIncomingMessageId,
maxReadOutgoingMessageId: readOutboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
},
unreadCount: Int(unreadCount)
))
}
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> switchToLatest
}
}
self.updateInitialStateDisposable = (updateInitialState
|> deliverOnMainQueue).start(next: { [weak self] updatedData in
guard let strongSelf = self else {
return
}
if let maxReadOutgoingMessageId = updatedData.maxReadOutgoingMessageId {
if let current = strongSelf.maxReadOutgoingMessageIdValue {
if maxReadOutgoingMessageId > current {
strongSelf.maxReadOutgoingMessageIdValue = maxReadOutgoingMessageId
}
} else {
strongSelf.maxReadOutgoingMessageIdValue = maxReadOutgoingMessageId
}
}
})
}
deinit {
self.initialStateDisposable?.dispose()
self.holesDisposable?.dispose()
self.readDisposable.dispose()
self.updateInitialStateDisposable?.dispose()
}
func setCurrentHole(entry: MessageHistoryHolesViewEntry?) {
if self.currentHole?.0 != entry {
self.currentHole?.1.dispose()
if let entry = entry {
self.currentHole = (entry, self.fetchHole(entry: entry).start(next: { [weak self] removedHoleIndices in
guard let strongSelf = self, let removedHoleIndices = removedHoleIndices else {
return
}
if var currentHoles = strongSelf.stateValue?.holeIndices[Namespaces.Message.Cloud] {
currentHoles.subtract(removedHoleIndices.removedIndices)
strongSelf.stateValue?.holeIndices[Namespaces.Message.Cloud] = currentHoles
}
}))
} else {
self.currentHole = nil
}
}
}
private func fetchHole(entry: MessageHistoryHolesViewEntry) -> Signal<FetchMessageHistoryHoleResult?, NoError> {
switch entry.hole {
case let .peer(hole):
let fetchCount = min(entry.count, 100)
return fetchMessageHistoryHole(accountPeerId: self.account.peerId, source: .network(self.account.network), postbox: self.account.postbox, peerInput: .direct(peerId: hole.peerId, threadId: hole.threadId), namespace: hole.namespace, direction: entry.direction, space: entry.space, count: fetchCount)
}
}
func applyMaxReadIndex(messageIndex: MessageIndex) {
let peerId = self.peerId
let threadId = self.threadId
if messageIndex.id.namespace != Namespaces.Message.Cloud {
return
}
let fromIdExclusive: Int32?
let toIndex = messageIndex
if let maxReadIncomingMessageId = self.maxReadIncomingMessageIdValue {
fromIdExclusive = maxReadIncomingMessageId.id
} else {
fromIdExclusive = nil
}
self.maxReadIncomingMessageIdValue = messageIndex.id
let account = self.account
let _ = (self.account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputPeer?, MessageId?, Int?) in
guard let peer = transaction.getPeer(peerId) else {
return (nil, nil, nil, nil)
}
var markMainAsRead = false
markMainAsRead = !"".isEmpty
if var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
if messageIndex.id.id >= data.maxIncomingReadId {
if let count = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: data.maxIncomingReadId, toIndex: messageIndex) {
data.incomingUnreadCount = max(0, data.incomingUnreadCount - Int32(count))
data.maxIncomingReadId = messageIndex.id.id
}
if let topMessageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) {
if messageIndex.id.id >= topMessageIndex.id.id {
let containingHole = transaction.getThreadIndexHole(peerId: peerId, threadId: threadId, namespace: topMessageIndex.id.namespace, containing: topMessageIndex.id.id)
if let _ = containingHole[.everywhere] {
} else {
data.incomingUnreadCount = 0
}
}
}
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
}
}
if markMainAsRead {
_internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: account.stateManager, index: messageIndex)
}
var subPeerId: Api.InputPeer?
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
subPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
} else {
let referencedMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))
if let message = transaction.getMessage(referencedMessageId) {
for attribute in message.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if let sourceMessage = transaction.getMessage(attribute.messageId) {
account.viewTracker.applyMaxReadIncomingMessageIdForReplyInfo(id: attribute.messageId, maxReadIncomingMessageId: messageIndex.id)
var updatedAttribute: ReplyThreadMessageAttribute?
for i in 0 ..< sourceMessage.attributes.count {
if let attribute = sourceMessage.attributes[i] as? ReplyThreadMessageAttribute {
if let maxReadMessageId = attribute.maxReadMessageId {
if maxReadMessageId < messageIndex.id.id {
updatedAttribute = ReplyThreadMessageAttribute(count: attribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: messageIndex.id.id)
}
} else {
updatedAttribute = ReplyThreadMessageAttribute(count: attribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: messageIndex.id.id)
}
break
}
}
if let updatedAttribute = updatedAttribute {
transaction.updateMessage(sourceMessage.id, update: { currentMessage in
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let _ = attributes[j] as? ReplyThreadMessageAttribute {
attributes[j] = updatedAttribute
}
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
break
}
}
}
}
let inputPeer = transaction.getPeer(messageIndex.id.peerId).flatMap(apiInputPeer)
let readCount = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: fromIdExclusive, toIndex: toIndex)
let topMessageId = transaction.getMessagesWithThreadId(peerId: peerId, namespace: Namespaces.Message.Cloud, threadId: threadId, from: MessageIndex.upperBound(peerId: peerId, namespace: Namespaces.Message.Cloud), includeFrom: false, to: MessageIndex.lowerBound(peerId: peerId, namespace: Namespaces.Message.Cloud), limit: 1).first?.id
return (inputPeer, subPeerId, topMessageId, readCount)
}
|> deliverOnMainQueue).start(next: { [weak self] inputPeer, subPeerId, topMessageId, readCount in
guard let strongSelf = self else {
return
}
guard let inputPeer else {
return
}
var revalidate = false
var unreadCountValue = strongSelf.unreadCountValue
if let readCount = readCount {
unreadCountValue = max(0, unreadCountValue - Int(readCount))
} else {
revalidate = true
}
if let topMessageId = topMessageId {
if topMessageId.id <= messageIndex.id.id {
unreadCountValue = 0
}
}
strongSelf.unreadCountValue = unreadCountValue
if let state = strongSelf.stateValue {
if let indices = state.holeIndices[messageIndex.id.namespace] {
let fromIdInt: Int
if let fromIdExclusive = fromIdExclusive {
fromIdInt = Int(fromIdExclusive + 1)
} else {
fromIdInt = 1
}
let toIdInt = Int(toIndex.id.id)
if fromIdInt <= toIdInt, indices.intersects(integersIn: fromIdInt ..< toIdInt) {
revalidate = true
}
}
}
if let subPeerId {
let signal = strongSelf.account.network.request(Api.functions.messages.readSavedHistory(parentPeer: inputPeer, peer: subPeerId, maxId: messageIndex.id.id))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
if revalidate {
}
strongSelf.readDisposable.set(signal.start())
} else {
var signal = strongSelf.account.network.request(Api.functions.messages.readDiscussion(peer: inputPeer, msgId: Int32(clamping: threadId), readMaxId: messageIndex.id.id))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
if revalidate {
let validateSignal = strongSelf.account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: Int32(clamping: threadId)))
|> map { result -> (MessageId?, Int) in
switch result {
case let .discussionMessage(_, _, _, readInboxMaxId, _, unreadCount, _, _):
return (readInboxMaxId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }), Int(unreadCount))
}
}
|> `catch` { _ -> Signal<(MessageId?, Int)?, NoError> in
return .single(nil)
}
|> afterNext { result in
guard let (incomingMessageId, count) = result else {
return
}
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.maxReadIncomingMessageIdValue = incomingMessageId
strongSelf.unreadCountValue = count
}
}
|> ignoreValues
signal = signal
|> then(validateSignal)
}
strongSelf.readDisposable.set(signal.start())
}
})
}
}
public class ReplyThreadHistoryContext {
fileprivate final class GuardReference {
private let deallocated: () -> Void
init(deallocated: @escaping () -> Void) {
self.deallocated = deallocated
}
deinit {
self.deallocated()
}
}
private let queue = Queue()
private let impl: QueueLocalObject<ReplyThreadHistoryContextImpl>
public var state: Signal<MessageHistoryViewExternalInput, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
let stateDisposable = impl.state.get().start(next: { state in
subscriber.putNext(MessageHistoryViewExternalInput(
content: .thread(peerId: state.messageId.peerId, id: Int64(state.messageId.id), holes: state.holeIndices),
maxReadIncomingMessageId: state.maxReadIncomingMessageId,
maxReadOutgoingMessageId: state.maxReadOutgoingMessageId
))
})
disposable.set(stateDisposable)
}
return disposable
}
}
public var maxReadOutgoingMessageId: Signal<MessageId?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.maxReadOutgoingMessageId.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public var unreadCount: Signal<Int, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.unreadCount.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public init(account: Account, peerId: PeerId, data: ChatReplyThreadMessage) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return ReplyThreadHistoryContextImpl(queue: queue, account: account, data: data)
})
}
public func applyMaxReadIndex(messageIndex: MessageIndex) {
self.impl.with { impl in
impl.applyMaxReadIndex(messageIndex: messageIndex)
}
}
}
public struct ChatReplyThreadMessage: Equatable {
public enum Anchor: Equatable {
case automatic
case lowerBoundMessage(MessageIndex)
}
public var peerId: PeerId
public var threadId: Int64
public var channelMessageId: MessageId?
public var isChannelPost: Bool
public var isForumPost: Bool
public var isMonoforumPost: Bool
public var maxMessage: MessageId?
public var maxReadIncomingMessageId: MessageId?
public var maxReadOutgoingMessageId: MessageId?
public var unreadCount: Int
public var initialFilledHoles: IndexSet
public var initialAnchor: Anchor
public var isNotAvailable: Bool
public var effectiveMessageId: MessageId? {
if self.peerId.namespace == Namespaces.Peer.CloudChannel || self.isForumPost {
return MessageId(peerId: self.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: self.threadId))
} else {
return nil
}
}
public init(peerId: PeerId, threadId: Int64, channelMessageId: MessageId?, isChannelPost: Bool, isForumPost: Bool, isMonoforumPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, unreadCount: Int, initialFilledHoles: IndexSet, initialAnchor: Anchor, isNotAvailable: Bool) {
self.peerId = peerId
self.threadId = threadId
self.channelMessageId = channelMessageId
self.isChannelPost = isChannelPost
self.isForumPost = isForumPost
self.isMonoforumPost = isMonoforumPost
self.maxMessage = maxMessage
self.maxReadIncomingMessageId = maxReadIncomingMessageId
self.maxReadOutgoingMessageId = maxReadOutgoingMessageId
self.unreadCount = unreadCount
self.initialFilledHoles = initialFilledHoles
self.initialAnchor = initialAnchor
self.isNotAvailable = isNotAvailable
}
public var normalized: ChatReplyThreadMessage {
if self.isForumPost {
return ChatReplyThreadMessage(peerId: self.peerId, threadId: self.threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, isMonoforumPost: self.isMonoforumPost, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)
} else {
return self
}
}
}
public enum FetchChannelReplyThreadMessageError {
case generic
}
func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: MessageId, atMessageId: MessageId?) -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> {
let accountPeerId = account.peerId
return account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(messageId.peerId)
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { peer -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
guard let peer = peer else {
return .fail(.generic)
}
guard let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
let replyInfo = Promise<MessageHistoryThreadData?>()
replyInfo.set(account.postbox.transaction { transaction -> MessageHistoryThreadData? in
return transaction.getMessageHistoryThreadInfo(peerId: messageId.peerId, threadId: Int64(messageId.id))?.data.get(MessageHistoryThreadData.self)
})
let remoteDiscussionMessageSignal: Signal<DiscussionMessage?, NoError> = account.network.request(Api.functions.messages.getDiscussionMessage(peer: inputPeer, msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.DiscussionMessage?, NoError> in
return .single(nil)
}
|> mapToSignal { discussionMessage -> Signal<DiscussionMessage?, NoError> in
guard let discussionMessage = discussionMessage else {
return .single(nil)
}
return account.postbox.transaction { transaction -> DiscussionMessage? in
switch discussionMessage {
case let .discussionMessage(_, messages, maxId, readInboxMaxId, readOutboxMaxId, unreadCount, chats, users):
let parsedMessages = messages.compactMap { message -> StoreMessage? in
StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum)
}
guard let topMessage = parsedMessages.last, let parsedIndex = topMessage.index else {
return nil
}
var channelMessageId: MessageId?
for attribute in topMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
channelMessageId = attribute.messageId
break
}
}
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
let _ = transaction.addMessages(parsedMessages, location: .Random)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let resolvedMaxMessage: MessageId?
if let maxId = maxId {
resolvedMaxMessage = MessageId(
peerId: parsedIndex.id.peerId,
namespace: Namespaces.Message.Cloud,
id: maxId
)
} else {
resolvedMaxMessage = nil
}
var isChannelPost = false
for attribute in topMessage.attributes {
if let _ = attribute as? SourceReferenceMessageAttribute {
isChannelPost = true
break
}
}
var isForumPost = false
var isMonoforumPost = false
if let channel = transaction.getPeer(parsedIndex.id.peerId) as? TelegramChannel {
if channel.isForumOrMonoForum {
isForumPost = true
}
if channel.isMonoForum {
isMonoforumPost = true
}
} else if let user = transaction.getPeer(parsedIndex.id.peerId) as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum) {
isForumPost = true
}
return DiscussionMessage(
messageId: parsedIndex.id,
channelMessageId: channelMessageId,
isChannelPost: isChannelPost,
isForumPost: isForumPost,
isMonoforumPost: isMonoforumPost,
maxMessage: resolvedMaxMessage,
maxReadIncomingMessageId: readInboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
},
maxReadOutgoingMessageId: readOutboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
},
unreadCount: Int(unreadCount)
)
}
}
}
let discussionMessageSignal = (replyInfo.get()
|> take(1)
|> mapToSignal { threadData -> Signal<DiscussionMessage?, NoError> in
guard let threadData = threadData else {
return .single(nil)
}
return account.postbox.transaction { transaction -> DiscussionMessage? in
return DiscussionMessage(
messageId: messageId,
channelMessageId: nil,
isChannelPost: false,
isForumPost: true,
isMonoforumPost: peer.isMonoForum,
maxMessage: MessageId(peerId: messageId.peerId, namespace: messageId.namespace, id: threadData.maxKnownMessageId),
maxReadIncomingMessageId: MessageId(peerId: messageId.peerId, namespace: messageId.namespace, id: threadData.maxIncomingReadId),
maxReadOutgoingMessageId: MessageId(peerId: messageId.peerId, namespace: messageId.namespace, id: threadData.maxOutgoingReadId),
unreadCount: Int(threadData.incomingUnreadCount)
)
/*var foundDiscussionMessageId: MessageId?
transaction.scanMessageAttributes(peerId: replyInfo.commentsPeerId, namespace: Namespaces.Message.Cloud, limit: 1000, { id, attributes in
for attribute in attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if attribute.messageId == messageId {
foundDiscussionMessageId = id
return true
}
}
}
if foundDiscussionMessageId != nil {
return false
}
return true
})
guard let discussionMessageId = foundDiscussionMessageId else {
return nil
}
return DiscussionMessage(
messageId: discussionMessageId,
channelMessageId: messageId,
isChannelPost: true,
maxMessage: replyInfo.maxMessageId,
maxReadIncomingMessageId: replyInfo.maxReadIncomingMessageId,
maxReadOutgoingMessageId: nil,
unreadCount: 0
)*/
}
})
|> mapToSignal { result -> Signal<DiscussionMessage?, NoError> in
if let result = result {
return .single(result)
} else {
return remoteDiscussionMessageSignal
}
}
let discussionMessage = Promise<DiscussionMessage?>()
discussionMessage.set(discussionMessageSignal)
enum Anchor {
case message(MessageId)
case lowerBound
case upperBound
}
let preloadedHistoryPosition: Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> = replyInfo.get()
|> take(1)
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { threadData -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> in
if let _ = threadData, !"".isEmpty {
return .fail(.generic)
} else {
return discussionMessage.get()
|> take(1)
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { discussionMessage -> Signal<(FetchMessageHistoryHoleThreadInput, PeerId, MessageId?, Anchor, MessageId?), FetchChannelReplyThreadMessageError> in
guard let discussionMessage = discussionMessage else {
return .fail(.generic)
}
let topMessageId = discussionMessage.messageId
let commentsPeerId = topMessageId.peerId
let anchor: Anchor
if let atMessageId = atMessageId {
anchor = .message(atMessageId)
} else if let maxReadIncomingMessageId = discussionMessage.maxReadIncomingMessageId {
anchor = .message(maxReadIncomingMessageId)
} else {
anchor = .lowerBound
}
return .single((.direct(peerId: commentsPeerId, threadId: Int64(topMessageId.id)), commentsPeerId, discussionMessage.messageId, anchor, discussionMessage.maxMessage))
}
}
}
let preloadedHistory: Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError>
preloadedHistory = preloadedHistoryPosition
|> mapToSignal { peerInput, commentsPeerId, threadMessageId, anchor, maxMessageId -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
guard let maxMessageId = maxMessageId else {
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(integersIn: 1 ..< Int(Int32.max - 1)), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil, ids: []), .automatic))
}
return account.postbox.transaction { transaction -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
if let threadMessageId = threadMessageId {
var holes = transaction.getThreadIndexHoles(peerId: threadMessageId.peerId, threadId: Int64(threadMessageId.id), namespace: Namespaces.Message.Cloud)
holes.remove(integersIn: Int(maxMessageId.id + 1) ..< Int(Int32.max))
let isParticipant = transaction.getPeerChatListIndex(commentsPeerId) != nil
if isParticipant {
let historyHoles = transaction.getHoles(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud)
holes.formIntersection(historyHoles)
}
let inputAnchor: HistoryViewInputAnchor
switch anchor {
case .lowerBound:
inputAnchor = .lowerBound
case .upperBound:
inputAnchor = .upperBound
case let .message(id):
inputAnchor = .message(id)
}
let testView = transaction.getMessagesHistoryViewState(
input: .external(MessageHistoryViewExternalInput(
content: .thread(
peerId: commentsPeerId,
id: Int64(threadMessageId.id),
holes: [
Namespaces.Message.Cloud: holes
]
),
maxReadIncomingMessageId: nil,
maxReadOutgoingMessageId: nil
)),
ignoreMessagesInTimestampRange: nil,
ignoreMessageIds: Set(),
count: 40,
clipHoles: true,
anchor: inputAnchor,
namespaces: .not(Namespaces.Message.allNonRegular)
)
if !testView.isLoading || transaction.getMessageHistoryThreadInfo(peerId: threadMessageId.peerId, threadId: Int64(threadMessageId.id)) != nil {
let initialAnchor: ChatReplyThreadMessage.Anchor
switch anchor {
case .lowerBound:
if let entry = testView.entries.first {
initialAnchor = .lowerBoundMessage(entry.index)
} else {
initialAnchor = .automatic
}
case .upperBound:
initialAnchor = .automatic
case .message:
initialAnchor = .automatic
}
return .single((FetchMessageHistoryHoleResult(removedIndices: IndexSet(), strictRemovedIndices: IndexSet(), actualPeerId: nil, actualThreadId: nil, ids: []), initialAnchor))
}
}
let direction: MessageHistoryViewRelativeHoleDirection
switch anchor {
case .lowerBound:
direction = .range(start: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: 1), end: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: Int32.max - 1))
case .upperBound:
direction = .range(start: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: Int32.max - 1), end: MessageId(peerId: commentsPeerId, namespace: Namespaces.Message.Cloud, id: 1))
case let .message(id):
direction = .aroundId(id)
}
return fetchMessageHistoryHole(accountPeerId: account.peerId, source: .network(account.network), postbox: account.postbox, peerInput: peerInput, namespace: Namespaces.Message.Cloud, direction: direction, space: .everywhere, count: 40)
|> castError(FetchChannelReplyThreadMessageError.self)
|> mapToSignal { result -> Signal<(FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor), FetchChannelReplyThreadMessageError> in
return account.postbox.transaction { transaction -> (FetchMessageHistoryHoleResult?, ChatReplyThreadMessage.Anchor) in
guard let result = result else {
return (nil, .automatic)
}
let initialAnchor: ChatReplyThreadMessage.Anchor
switch anchor {
case .lowerBound:
if let actualPeerId = result.actualPeerId, let actualThreadId = result.actualThreadId {
if let firstMessage = transaction.getMessagesWithThreadId(peerId: actualPeerId, namespace: Namespaces.Message.Cloud, threadId: actualThreadId, from: MessageIndex.lowerBound(peerId: actualPeerId, namespace: Namespaces.Message.Cloud), includeFrom: false, to: MessageIndex.upperBound(peerId: actualPeerId, namespace: Namespaces.Message.Cloud), limit: 1).first {
initialAnchor = .lowerBoundMessage(firstMessage.index)
} else {
initialAnchor = .automatic
}
} else {
initialAnchor = .automatic
}
case .upperBound:
initialAnchor = .automatic
case .message:
initialAnchor = .automatic
}
return (result, initialAnchor)
}
|> castError(FetchChannelReplyThreadMessageError.self)
}
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> switchToLatest
}
return combineLatest(
discussionMessage.get()
|> take(1)
|> castError(FetchChannelReplyThreadMessageError.self),
preloadedHistory
)
|> mapToSignal { discussionMessage, initialFilledHolesAndInitialAnchor -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
guard let discussionMessage = discussionMessage else {
return .fail(.generic)
}
let (initialFilledHoles, initialAnchor) = initialFilledHolesAndInitialAnchor
return account.postbox.transaction { transaction -> Signal<ChatReplyThreadMessage, FetchChannelReplyThreadMessageError> in
if let initialFilledHoles = initialFilledHoles {
for range in initialFilledHoles.strictRemovedIndices.rangeView {
transaction.removeHole(peerId: discussionMessage.messageId.peerId, threadId: Int64(discussionMessage.messageId.id), namespace: Namespaces.Message.Cloud, space: .everywhere, range: Int32(range.lowerBound) ... Int32(range.upperBound))
}
}
return .single(ChatReplyThreadMessage(
peerId: discussionMessage.messageId.peerId,
threadId: Int64(discussionMessage.messageId.id),
channelMessageId: discussionMessage.channelMessageId,
isChannelPost: discussionMessage.isChannelPost,
isForumPost: discussionMessage.isForumPost,
isMonoforumPost: discussionMessage.isMonoforumPost,
maxMessage: discussionMessage.maxMessage,
maxReadIncomingMessageId: discussionMessage.maxReadIncomingMessageId,
maxReadOutgoingMessageId: discussionMessage.maxReadOutgoingMessageId,
unreadCount: discussionMessage.unreadCount,
initialFilledHoles: initialFilledHoles?.removedIndices ?? IndexSet(),
initialAnchor: initialAnchor,
isNotAvailable: initialFilledHoles == nil
))
}
|> castError(FetchChannelReplyThreadMessageError.self)
|> switchToLatest
}
}
}
@@ -0,0 +1,45 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
public enum ReportAdMessageResult {
public struct Option: Equatable {
public let text: String
public let option: Data
}
case options(title: String, options: [Option])
case adsHidden
case reported
}
public enum ReportAdMessageError {
case generic
case premiumRequired
}
func _internal_reportAdMessage(account: Account, opaqueId: Data, option: Data?) -> Signal<ReportAdMessageResult, ReportAdMessageError> {
return account.network.request(Api.functions.messages.reportSponsoredMessage(randomId: Buffer(data: opaqueId), option: Buffer(data: option)))
|> mapError { error -> ReportAdMessageError in
if error.errorDescription == "PREMIUM_ACCOUNT_REQUIRED" {
return .premiumRequired
}
return .generic
}
|> map { result -> ReportAdMessageResult in
switch result {
case let .sponsoredMessageReportResultChooseOption(title, options):
return .options(title: title, options: options.map {
switch $0 {
case let .sponsoredMessageReportOption(text, option):
return ReportAdMessageResult.Option(text: text, option: option.makeData())
}
})
case .sponsoredMessageReportResultAdsHidden:
return .adsHidden
case .sponsoredMessageReportResultReported:
return .reported
}
}
}
@@ -0,0 +1,82 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum ReportContentResult {
public struct Option: Equatable {
public let text: String
public let option: Data
}
case options(title: String, options: [Option])
case addComment(optional: Bool, option: Data)
case reported
}
public enum ReportContentError {
case generic
case messageIdRequired
}
public enum ReportContentSubject: Equatable {
case peer(EnginePeer.Id)
case messages([EngineMessage.Id])
case stories(EnginePeer.Id, [Int32])
public var peerId: EnginePeer.Id {
switch self {
case let .peer(peerId):
return peerId
case let .messages(messageIds):
return messageIds.first!.peerId
case let .stories(peerId, _):
return peerId
}
}
}
func _internal_reportContent(account: Account, subject: ReportContentSubject, option: Data?, message: String?) -> Signal<ReportContentResult, ReportContentError> {
return account.postbox.transaction { transaction -> Signal<ReportContentResult, ReportContentError> in
guard let peer = transaction.getPeer(subject.peerId), let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
let request: Signal<Api.ReportResult, MTRpcError>
if case let .stories(_, ids) = subject {
request = account.network.request(Api.functions.stories.report(peer: inputPeer, id: ids, option: Buffer(data: option), message: message ?? ""))
} else {
var ids: [Int32] = []
if case let .messages(messageIds) = subject {
ids = messageIds.map { $0.id }
}
request = account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids, option: Buffer(data: option), message: message ?? ""))
}
return request
|> mapError { error -> ReportContentError in
if error.errorDescription == "MESSAGE_ID_REQUIRED" {
return .messageIdRequired
}
return .generic
}
|> map { result -> ReportContentResult in
switch result {
case let .reportResultChooseOption(title, options):
return .options(title: title, options: options.map {
switch $0 {
case let .messageReportOption(text, option):
return ReportContentResult.Option(text: text, option: option.makeData())
}
})
case let .reportResultAddComment(flags, option):
return .addComment(optional: (flags & (1 << 0)) != 0, option: option.makeData())
case .reportResultReported:
return .reported
}
}
}
|> castError(ReportContentError.self)
|> switchToLatest
}
@@ -0,0 +1,35 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
public func _internal_reportMessageDelivery(postbox: Postbox, network: Network, messageIds: [EngineMessage.Id], fromPushNotification: Bool) -> Signal<Bool, NoError> {
var signals: [Signal<Void, NoError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
signals.append(_internal_reportMessageDeliveryByPeerId(postbox: postbox, network: network, peerId: peerId, messageIds: messageIds, fromPushNotification: fromPushNotification))
}
return combineLatest(signals)
|> mapToSignal { _ in
return .single(true)
}
}
private func _internal_reportMessageDeliveryByPeerId(postbox: Postbox, network: Network, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id], fromPushNotification: Bool) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else {
return .complete()
}
var flags: Int32 = 0
if fromPushNotification {
flags |= (1 << 0)
}
return network.request(Api.functions.messages.reportMessagesDelivery(flags: flags, peer: inputPeer, id: messageIds.map { $0.id }))
|> `catch` { error -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ in
return .complete()
}
}
|> switchToLatest
}
@@ -0,0 +1,167 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum RequestChatContextResultsError {
case generic
case locationRequired
}
public final class CachedChatContextResult: Codable {
public let data: Data
public let timestamp: Int32
public init(data: Data, timestamp: Int32) {
self.data = data
self.timestamp = timestamp
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.data = try container.decode(Data.self, forKey: "data")
self.timestamp = try container.decode(Int32.self, forKey: "timestamp")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.data, forKey: "data")
try container.encode(self.timestamp, forKey: "timestamp")
}
}
private struct RequestData: Codable {
let version: String
let botId: PeerId
let peerId: PeerId
let query: String
}
private let requestVersion = "3"
public struct RequestChatContextResultsResult {
public let results: ChatContextResultCollection
public let isStale: Bool
public init(results: ChatContextResultCollection, isStale: Bool) {
self.results = results
self.isStale = isStale
}
}
func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> {
return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in
if let bot = transaction.getPeer(botId), let peer = transaction.getPeer(peerId) {
return (bot, peer)
} else {
return nil
}
}
|> mapToSignal { botAndPeer -> Signal<((bot: Peer, peer: Peer)?, (Double, Double)?), NoError> in
if let (bot, _) = botAndPeer, let botUser = bot as? TelegramUser, let botInfo = botUser.botInfo, botInfo.flags.contains(.requiresGeolocationForInlineRequests) {
return location
|> take(1)
|> map { location -> ((bot: Peer, peer: Peer)?, (Double, Double)?) in
return (botAndPeer, location)
}
} else {
return .single((botAndPeer, nil))
}
}
|> castError(RequestChatContextResultsError.self)
|> mapToSignal { botAndPeer, location -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> in
guard let (bot, peer) = botAndPeer, let inputBot = apiInputUser(bot) else {
return .single(nil)
}
return account.postbox.transaction { transaction -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> in
var staleResult: RequestChatContextResultsResult?
if offset.isEmpty && location == nil {
let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query)
if let keyData = try? JSONEncoder().encode(requestData) {
let key = ValueBoxKey(MemoryBuffer(data: keyData))
if let cachedEntry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key))?.get(CachedChatContextResult.self) {
if let cachedResult = try? JSONDecoder().decode(ChatContextResultCollection.self, from: cachedEntry.data) {
let timestamp = Int32(Date().timeIntervalSince1970)
if cachedEntry.timestamp + cachedResult.cacheTimeout > timestamp {
return .single(RequestChatContextResultsResult(results: cachedResult, isStale: false))
} else if staleCachedResults {
let staleCollection = ChatContextResultCollection(
botId: cachedResult.botId,
peerId: cachedResult.peerId,
query: cachedResult.query,
geoPoint: cachedResult.geoPoint,
queryId: cachedResult.queryId,
nextOffset: nil,
presentation: cachedResult.presentation,
switchPeer: cachedResult.switchPeer,
webView: cachedResult.webView,
results: cachedResult.results,
cacheTimeout: 0
)
staleResult = RequestChatContextResultsResult(results: staleCollection, isStale: true)
}
}
}
}
}
var flags: Int32 = 0
var inputPeer: Api.InputPeer = .inputPeerEmpty
var geoPoint: Api.InputGeoPoint?
if let actualInputPeer = apiInputPeer(peer) {
inputPeer = actualInputPeer
}
if let (latitude, longitude) = location {
flags |= (1 << 0)
let geoPointFlags: Int32 = 0
geoPoint = Api.InputGeoPoint.inputGeoPoint(flags: geoPointFlags, lat: latitude, long: longitude, accuracyRadius: nil)
}
var signal: Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> = account.network.request(Api.functions.messages.getInlineBotResults(flags: flags, bot: inputBot, peer: inputPeer, geoPoint: geoPoint, query: query, offset: offset))
|> map { result -> ChatContextResultCollection? in
return ChatContextResultCollection(apiResults: result, botId: bot.id, peerId: peerId, query: query, geoPoint: location)
}
|> mapError { error -> RequestChatContextResultsError in
if error.errorDescription == "BOT_INLINE_GEO_REQUIRED" {
return .locationRequired
} else {
return .generic
}
}
|> mapToSignal { result -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> in
guard let result = result else {
return .single(nil)
}
return account.postbox.transaction { transaction -> RequestChatContextResultsResult? in
if result.cacheTimeout > 10, offset.isEmpty && location == nil {
if let resultData = try? JSONEncoder().encode(result) {
let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query)
if let keyData = try? JSONEncoder().encode(requestData) {
let key = ValueBoxKey(MemoryBuffer(data: keyData))
if let entry = CodableEntry(CachedChatContextResult(data: resultData, timestamp: Int32(Date().timeIntervalSince1970))) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key), entry: entry)
}
}
}
}
return RequestChatContextResultsResult(results: result, isStale: false)
}
|> castError(RequestChatContextResultsError.self)
}
if incompleteResults {
signal = .single(staleResult) |> then(signal)
}
return signal
}
|> castError(RequestChatContextResultsError.self)
|> switchToLatest
}
}
@@ -0,0 +1,267 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum MessageActionCallbackResult {
case none
case alert(String)
case toast(String)
case url(String)
}
public enum MessageActionCallbackError {
case generic
case twoStepAuthMissing
case twoStepAuthTooFresh(Int32)
case authSessionTooFresh(Int32)
case limitExceeded
case requestPassword
case invalidPassword
case restricted
case userBlocked
}
func _internal_requestMessageActionCallbackPasswordCheck(account: Account, messageId: MessageId, isGame: Bool, data: MemoryBuffer?) -> Signal<Never, MessageActionCallbackError> {
return account.postbox.loadedPeerWithId(messageId.peerId)
|> castError(MessageActionCallbackError.self)
|> take(1)
|> mapToSignal { peer in
if let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0
var dataBuffer: Buffer?
if let data = data {
flags |= Int32(1 << 0)
dataBuffer = Buffer(data: data.makeData())
}
if isGame {
flags |= Int32(1 << 1)
}
return account.network.request(Api.functions.messages.getBotCallbackAnswer(flags: flags, peer: inputPeer, msgId: messageId.id, data: dataBuffer, password: .inputCheckPasswordEmpty))
|> mapError { error -> MessageActionCallbackError 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)
}
} else if error.errorDescription == "USER_PRIVACY_RESTRICTED" {
return .restricted
} else if error.errorDescription == "USER_BLOCKED" {
return .userBlocked
}
return .generic
}
|> mapToSignal { _ -> Signal<Never, MessageActionCallbackError> in
return .complete()
}
} else {
return .fail(.generic)
}
}
}
func _internal_requestMessageActionCallback(account: Account, messageId: MessageId, isGame :Bool, password: String?, data: MemoryBuffer?) -> Signal<MessageActionCallbackResult, MessageActionCallbackError> {
return account.postbox.loadedPeerWithId(messageId.peerId)
|> castError(MessageActionCallbackError.self)
|> take(1)
|> mapToSignal { peer in
if let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0
var dataBuffer: Buffer?
if let data = data {
flags |= Int32(1 << 0)
dataBuffer = Buffer(data: data.makeData())
}
if isGame {
flags |= Int32(1 << 1)
}
let checkPassword: Signal<Api.InputCheckPasswordSRP?, MessageActionCallbackError>
if let password = password, !password.isEmpty {
flags |= Int32(1 << 2)
checkPassword = _internal_twoStepAuthData(account.network)
|> mapError { error -> MessageActionCallbackError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else {
return .generic
}
}
|> mapToSignal { authData -> Signal<Api.InputCheckPasswordSRP?, MessageActionCallbackError> 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)
}
}
} else {
checkPassword = .single(nil)
}
return checkPassword
|> mapToSignal { password -> Signal<MessageActionCallbackResult, MessageActionCallbackError> in
return account.network.request(Api.functions.messages.getBotCallbackAnswer(flags: flags, peer: inputPeer, msgId: messageId.id, data: dataBuffer, password: password))
|> map(Optional.init)
|> mapError { error -> MessageActionCallbackError in
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)
}
} else if error.errorDescription == "USER_PRIVACY_RESTRICTED" {
return .restricted
} else if error.errorDescription == "USER_BLOCKED" {
return .userBlocked
}
return .generic
}
|> map { result -> MessageActionCallbackResult in
guard let result = result else {
return .none
}
switch result {
case let .botCallbackAnswer(flags, message, url, _):
if let message = message {
if (flags & (1 << 1)) != 0 {
return .alert(message)
} else {
return .toast(message)
}
} else if let url = url {
return .url(url)
} else {
return .none
}
}
}
}
} else {
return .single(.none)
}
}
}
public enum MessageActionUrlAuthResult {
case `default`
case accepted(String)
case request(String, Peer, Bool)
}
public enum MessageActionUrlSubject {
case message(id: MessageId, buttonId: Int32)
case url(String)
}
func _internal_requestMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject) -> Signal<MessageActionUrlAuthResult, NoError> {
let request: Signal<Api.UrlAuthResult?, MTRpcError>
var flags: Int32 = 0
switch subject {
case let .message(messageId, buttonId):
flags |= (1 << 1)
request = account.postbox.loadedPeerWithId(messageId.peerId)
|> take(1)
|> castError(MTRpcError.self)
|> mapToSignal { peer -> Signal<Api.UrlAuthResult?, MTRpcError> in
if let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.requestUrlAuth(flags: flags, peer: inputPeer, msgId: messageId.id, buttonId: buttonId, url: nil))
|> map(Optional.init)
} else {
return .single(nil)
}
}
case let .url(url):
flags |= (1 << 2)
request = account.network.request(Api.functions.messages.requestUrlAuth(flags: flags, peer: nil, msgId: nil, buttonId: nil, url: url))
|> map(Optional.init)
}
return request
|> `catch` { _ -> Signal<Api.UrlAuthResult?, NoError> in
return .single(nil)
}
|> map { result -> MessageActionUrlAuthResult in
guard let result = result else {
return .default
}
switch result {
case .urlAuthResultDefault:
return .default
case let .urlAuthResultAccepted(url):
return .accepted(url)
case let .urlAuthResultRequest(flags, bot, domain):
return .request(domain, TelegramUser(user: bot), (flags & (1 << 0)) != 0)
}
}
}
func _internal_acceptMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject, allowWriteAccess: Bool) -> Signal<MessageActionUrlAuthResult, NoError> {
var flags: Int32 = 0
if allowWriteAccess {
flags |= Int32(1 << 0)
}
let request: Signal<Api.UrlAuthResult?, MTRpcError>
switch subject {
case let .message(messageId, buttonId):
flags |= (1 << 1)
request = account.postbox.loadedPeerWithId(messageId.peerId)
|> take(1)
|> castError(MTRpcError.self)
|> mapToSignal { peer -> Signal<Api.UrlAuthResult?, MTRpcError> in
if let inputPeer = apiInputPeer(peer) {
let flags: Int32 = 1 << 1
return account.network.request(Api.functions.messages.acceptUrlAuth(flags: flags, peer: inputPeer, msgId: messageId.id, buttonId: buttonId, url: nil))
|> map(Optional.init)
} else {
return .single(nil)
}
}
case let .url(url):
flags |= (1 << 2)
request = account.network.request(Api.functions.messages.acceptUrlAuth(flags: flags, peer: nil, msgId: nil, buttonId: nil, url: url))
|> map(Optional.init)
}
return request
|> `catch` { _ -> Signal<Api.UrlAuthResult?, NoError> in
return .single(nil)
}
|> map { result -> MessageActionUrlAuthResult in
guard let result = result else {
return .default
}
switch result {
case let .urlAuthResultAccepted(url):
return .accepted(url)
default:
return .default
}
}
}
@@ -0,0 +1,82 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func _internal_requestStartBot(account: Account, botPeerId: PeerId, payload: String?) -> Signal<Void, NoError> {
if let payload = payload, !payload.isEmpty {
return account.postbox.loadedPeerWithId(botPeerId)
|> mapToSignal { botPeer -> Signal<Void, NoError> in
if let inputUser = apiInputUser(botPeer) {
return account.network.request(Api.functions.messages.startBot(bot: inputUser, peer: .inputPeerEmpty, randomId: Int64.random(in: Int64.min ... Int64.max), startParam: payload))
|> mapToSignal { result -> Signal<Void, MTRpcError> in
account.stateManager.addUpdates(result)
return .complete()
}
|> `catch` { _ -> Signal<Void, MTRpcError> in
return .complete()
}
|> retryRequest
} else {
return .complete()
}
}
} else {
return enqueueMessages(account: account, peerId: botPeerId, messages: [.message(text: "/start", attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) |> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
}
public enum RequestStartBotInGroupError {
case generic
}
public enum StartBotInGroupResult {
case none
case channelParticipant(RenderedChannelParticipant)
}
func _internal_requestStartBotInGroup(account: Account, botPeerId: PeerId, groupPeerId: PeerId, payload: String?) -> Signal<StartBotInGroupResult, RequestStartBotInGroupError> {
return account.postbox.transaction { transaction -> (Peer?, Peer?) in
return (transaction.getPeer(botPeerId), transaction.getPeer(groupPeerId))
}
|> mapError { _ -> RequestStartBotInGroupError in }
|> mapToSignal { botPeer, groupPeer -> Signal<StartBotInGroupResult, RequestStartBotInGroupError> in
if let botPeer = botPeer, let inputUser = apiInputUser(botPeer), let groupPeer = groupPeer, let inputGroup = apiInputPeer(groupPeer) {
let request = account.network.request(Api.functions.messages.startBot(bot: inputUser, peer: inputGroup, randomId: Int64.random(in: Int64.min ... Int64.max), startParam: payload ?? ""))
|> mapError { _ -> RequestStartBotInGroupError in
return .generic
}
|> mapToSignal { result -> Signal<StartBotInGroupResult, RequestStartBotInGroupError> in
account.stateManager.addUpdates(result)
if groupPeerId.namespace == Namespaces.Peer.CloudChannel {
return _internal_fetchChannelParticipant(account: account, peerId: groupPeerId, participantId: botPeerId)
|> mapError { _ -> RequestStartBotInGroupError in }
|> mapToSignal { participant -> Signal<StartBotInGroupResult, RequestStartBotInGroupError> in
return account.postbox.transaction { transaction -> StartBotInGroupResult in
if let participant = participant, let peer = transaction.getPeer(participant.peerId) {
var peers: [PeerId: Peer] = [:]
let presences: [PeerId: PeerPresence] = [:]
peers[peer.id] = peer
return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences))
} else {
return .none
}
}
|> mapError { _ -> RequestStartBotInGroupError in }
}
} else {
return .single(.none)
}
}
return request
} else {
return .complete()
}
}
}
@@ -0,0 +1,161 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
func _internal_sendScheduledMessageNowInteractively(postbox: Postbox, messageId: MessageId) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .sendScheduledMessageImmediately, id: messageId, action: SendScheduledMessageImmediatelyAction())
}
|> ignoreValues
}
private final class ManagedApplyPendingScheduledMessagesActionsHelper {
var operationDisposables: [MessageId: Disposable] = [:]
func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PendingMessageActionsEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validIds = Set<MessageId>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.id.peerId) {
hasRunningOperationForPeerId.insert(entry.id.peerId)
validIds.insert(entry.id)
if self.operationDisposables[entry.id] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.id] = disposable
}
}
}
var removeMergedIds: [MessageId] = []
for (id, disposable) in self.operationDisposables {
if !validIds.contains(id) {
removeMergedIds.append(id)
disposeOperations.append(disposable)
}
}
for id in removeMergedIds {
self.operationDisposables.removeValue(forKey: id)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal<Never, NoError>) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Signal<Never, NoError> in
var result: PendingMessageActionsEntry?
if let action = transaction.getPendingMessageAction(type: type, id: id) as? SendScheduledMessageImmediatelyAction {
result = PendingMessageActionsEntry(id: id, action: action)
}
return f(transaction, result)
}
|> switchToLatest
}
func managedApplyPendingScheduledMessagesActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedApplyPendingScheduledMessagesActionsHelper>(value: ManagedApplyPendingScheduledMessagesActionsHelper())
let actionsKey = PostboxViewKey.pendingMessageActions(type: .sendScheduledMessageImmediately)
let disposable = postbox.combinedView(keys: [actionsKey]).start(next: { view in
var entries: [PendingMessageActionsEntry] = []
if let v = view.views[actionsKey] as? PendingMessageActionsView {
entries = v.entries
}
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) in
return helper.update(entries: entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenAction(postbox: postbox, type: .sendScheduledMessageImmediately, id: entry.id, { transaction, entry -> Signal<Never, NoError> in
if let entry = entry {
if let _ = entry.action as? SendScheduledMessageImmediatelyAction {
return sendScheduledMessageNow(postbox: postbox, network: network, stateManager: stateManager, messageId: entry.id)
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
} else {
assertionFailure()
}
}
return .complete()
})
|> then(
postbox.transaction { transaction -> Void in
var resourceIds: [MediaResourceId] = []
transaction.deleteMessages([entry.id], forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
})
if !resourceIds.isEmpty {
let _ = postbox.mediaBox.removeCachedResources(Array(Set(resourceIds))).start()
}
}
|> ignoreValues
)
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private enum SendScheduledMessageNowError {
case generic
}
private func sendScheduledMessageNow(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal<Never, SendScheduledMessageNowError> {
return postbox.transaction { transaction -> Peer? in
guard let peer = transaction.getPeer(messageId.peerId) else {
return nil
}
return peer
}
|> castError(SendScheduledMessageNowError.self)
|> mapToSignal { peer -> Signal<Never, SendScheduledMessageNowError> in
guard let peer = peer else {
return .fail(.generic)
}
guard let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
return network.request(Api.functions.messages.sendScheduledMessages(peer: inputPeer, id: [messageId.id]))
|> mapError { _ -> SendScheduledMessageNowError in
return .generic
}
|> mapToSignal { updates -> Signal<Never, SendScheduledMessageNowError> in
stateManager.addUpdates(updates)
return .complete()
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,327 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public struct SendAsPeer: Equatable {
public let peer: Peer
public let subscribers: Int32?
public let isPremiumRequired: Bool
public init(peer: Peer, subscribers: Int32?, isPremiumRequired: Bool) {
self.peer = peer
self.subscribers = subscribers
self.isPremiumRequired = isPremiumRequired
}
public static func ==(lhs: SendAsPeer, rhs: SendAsPeer) -> Bool {
return lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers && lhs.isPremiumRequired == rhs.isPremiumRequired
}
}
public final class CachedSendAsPeers: Codable {
public let peerIds: [PeerId]
public let premiumRequiredPeerIds: Set<PeerId>
public let timestamp: Int32
public init(peerIds: [PeerId], premiumRequiredPeerIds: Set<PeerId>, timestamp: Int32) {
self.peerIds = peerIds
self.premiumRequiredPeerIds = premiumRequiredPeerIds
self.timestamp = timestamp
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.peerIds = (try container.decode([Int64].self, forKey: "peerIds")).map(PeerId.init)
self.premiumRequiredPeerIds = Set((try container.decodeIfPresent([Int64].self, forKey: "premiumRequiredPeerIds") ?? []).map(PeerId.init))
self.timestamp = try container.decode(Int32.self, forKey: "timestamp")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "peerIds")
try container.encode(Array(self.premiumRequiredPeerIds).map { $0.toInt64() }, forKey: "premiumRequiredPeerIds")
try container.encode(self.timestamp, forKey: "timestamp")
}
}
func _internal_cachedPeerSendAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[SendAsPeer], NoError> {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
return account.postbox.transaction { transaction -> ([SendAsPeer], Int32)? in
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedSendAsPeers, key: key))?.get(CachedSendAsPeers.self)
if let cached = cached {
var peers: [SendAsPeer] = []
for peerId in cached.peerIds {
if let peer = transaction.getPeer(peerId) {
var subscribers: Int32?
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
subscribers = cachedData.participantsSummary.memberCount
}
peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
}
}
return (peers, cached.timestamp)
} else {
return nil
}
}
|> mapToSignal { cachedPeersAndTimestamp -> Signal<[SendAsPeer], NoError> in
let initialSignal: Signal<[SendAsPeer], NoError>
if let (cachedPeers, _) = cachedPeersAndTimestamp {
initialSignal = .single(cachedPeers)
} else {
initialSignal = .complete()
}
return initialSignal
|> then(_internal_peerSendAsAvailablePeers(accountPeerId: account.peerId, network: account.network, postbox: account.postbox, peerId: peerId)
|> mapToSignal { peers -> Signal<[SendAsPeer], NoError> in
return account.postbox.transaction { transaction -> [SendAsPeer] in
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var premiumRequiredPeerIds = Set<PeerId>()
for peer in peers {
if peer.isPremiumRequired {
premiumRequiredPeerIds.insert(peer.peer.id)
}
}
if let entry = CodableEntry(CachedSendAsPeers(peerIds: peers.map { $0.peer.id }, premiumRequiredPeerIds: premiumRequiredPeerIds, timestamp: currentTimestamp)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedSendAsPeers, key: key), entry: entry)
}
return peers
}
})
}
}
func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, postbox: Postbox, peerId: PeerId) -> Signal<[SendAsPeer], NoError> {
return postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> mapToSignal { peer -> Signal<[SendAsPeer], NoError> in
guard let peer = peer else {
return .single([])
}
guard let inputPeer = apiInputPeer(peer) else {
return .single([])
}
if let channel = peer as? TelegramChannel {
if case .group = channel.info {
} else if channel.adminRights != nil || channel.flags.contains(.isCreator) {
} else {
return .single([])
}
} else {
return .single([])
}
return network.request(Api.functions.channels.getSendAs(flags: 0, peer: inputPeer))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.channels.SendAsPeers?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<[SendAsPeer], NoError> in
guard let result = result else {
return .single([])
}
switch result {
case let .sendAsPeers(sendAsPeers, chats, _):
return postbox.transaction { transaction -> [SendAsPeer] in
var subscribers: [PeerId: Int32] = [:]
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: [])
var premiumRequiredPeerIds = Set<PeerId>()
for sendAsPeer in sendAsPeers {
if case let .sendAsPeer(flags, peer) = sendAsPeer, (flags & (1 << 0)) != 0 {
premiumRequiredPeerIds.insert(peer.peerId)
}
}
for chat in chats {
if let groupOrChannel = parsedPeers.get(chat.peerId) {
switch chat {
case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _, _, _, _):
if let participantsCount = participantsCount {
subscribers[groupOrChannel.id] = participantsCount
}
case let .chat(_, _, _, _, participantsCount, _, _, _, _, _):
subscribers[groupOrChannel.id] = participantsCount
default:
break
}
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId,peers: parsedPeers)
var peers: [Peer] = []
for chat in chats {
if let peer = transaction.getPeer(chat.peerId) {
peers.append(peer)
}
}
return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
}
}
}
}
}
public enum UpdatePeerSendAsPeerError {
case generic
}
func _internal_updatePeerSendAsPeer(account: Account, peerId: PeerId, sendAs: PeerId) -> Signal<Never, UpdatePeerSendAsPeerError> {
return account.postbox.transaction { transaction -> (Api.InputPeer, Api.InputPeer)? in
if let peer = transaction.getPeer(peerId), let sendAsPeer = transaction.getPeer(sendAs), let inputPeer = apiInputPeer(peer), let sendAsInputPeer = apiInputPeerOrSelf(sendAsPeer, accountPeerId: account.peerId) {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
if let cachedData = cachedData as? CachedChannelData {
return cachedData.withUpdatedSendAsPeerId(sendAs)
} else {
return cachedData
}
})
return (inputPeer, sendAsInputPeer)
} else {
return nil
}
}
|> castError(UpdatePeerSendAsPeerError.self)
|> mapToSignal { result in
guard let (inputPeer, sendAsInputPeer) = result else {
return .fail(.generic)
}
return account.network.request(Api.functions.messages.saveDefaultSendAs(peer: inputPeer, sendAs: sendAsInputPeer))
|> mapError { _ -> UpdatePeerSendAsPeerError in
return .generic
}
|> mapToSignal { result -> Signal<Never, UpdatePeerSendAsPeerError> in
return account.postbox.transaction { transaction in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
if let cachedData = cachedData as? CachedChannelData {
return cachedData.withUpdatedSendAsPeerId(sendAs)
} else {
return cachedData
}
})
}
|> castError(UpdatePeerSendAsPeerError.self)
|> ignoreValues
}
}
}
func _internal_cachedLiveStorySendAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[SendAsPeer], NoError> {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
return account.postbox.transaction { transaction -> ([SendAsPeer], Int32)? in
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedLiveStorySendAsPeers, key: key))?.get(CachedSendAsPeers.self)
if let cached = cached {
var peers: [SendAsPeer] = []
for peerId in cached.peerIds {
if let peer = transaction.getPeer(peerId) {
var subscribers: Int32?
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
subscribers = cachedData.participantsSummary.memberCount
}
peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id)))
}
}
return (peers, cached.timestamp)
} else {
return nil
}
}
|> mapToSignal { cachedPeersAndTimestamp -> Signal<[SendAsPeer], NoError> in
let initialSignal: Signal<[SendAsPeer], NoError>
if let (cachedPeers, _) = cachedPeersAndTimestamp {
initialSignal = .single(cachedPeers)
} else {
initialSignal = .complete()
}
return initialSignal
|> then(_internal_liveStorySendAsAvailablePeers(account: account, peerId: peerId)
|> mapToSignal { peers -> Signal<[SendAsPeer], NoError> in
return account.postbox.transaction { transaction -> [SendAsPeer] in
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var premiumRequiredPeerIds = Set<PeerId>()
for peer in peers {
if peer.isPremiumRequired {
premiumRequiredPeerIds.insert(peer.peer.id)
}
}
if let entry = CodableEntry(CachedSendAsPeers(peerIds: peers.map { $0.peer.id }, premiumRequiredPeerIds: premiumRequiredPeerIds, timestamp: currentTimestamp)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedLiveStorySendAsPeers, key: key), entry: entry)
}
return peers
}
})
}
}
func _internal_liveStorySendAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[SendAsPeer], NoError> {
return account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> mapToSignal { peer -> Signal<[SendAsPeer], NoError> in
guard let peer else {
return .single([])
}
guard let inputPeer = apiInputPeer(peer) else {
return .single([])
}
return account.network.request(Api.functions.channels.getSendAs(flags: 1 << 1, peer: inputPeer))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.channels.SendAsPeers?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<[SendAsPeer], NoError> in
guard let result = result else {
return .single([])
}
switch result {
case let .sendAsPeers(sendAsPeers, chats, _):
return account.postbox.transaction { transaction -> [SendAsPeer] in
var subscribers: [PeerId: Int32] = [:]
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: [])
var premiumRequiredPeerIds = Set<PeerId>()
for sendAsPeer in sendAsPeers {
if case let .sendAsPeer(flags, peer) = sendAsPeer, (flags & (1 << 0)) != 0 {
premiumRequiredPeerIds.insert(peer.peerId)
}
}
for chat in chats {
if let groupOrChannel = parsedPeers.get(chat.peerId) {
switch chat {
case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _, _, _, _):
if let participantsCount = participantsCount {
subscribers[groupOrChannel.id] = participantsCount
}
case let .chat(_, _, _, _, participantsCount, _, _, _, _, _):
subscribers[groupOrChannel.id] = participantsCount
default:
break
}
}
}
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers)
var peers: [Peer] = []
for chat in chats {
if let peer = transaction.getPeer(chat.peerId) {
peers.append(peer)
}
}
return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) }
}
}
}
}
}
@@ -0,0 +1,974 @@
import SwiftSignalKit
import Postbox
import TelegramApi
public final class SparseMessageList {
private final class Impl {
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let threadId: Int64?
private let messageTag: MessageTags
private struct TopSection: Equatable {
var messages: [Message]
static func ==(lhs: TopSection, rhs: TopSection) -> Bool {
if lhs.messages.count != rhs.messages.count {
return false
}
for i in 0 ..< lhs.messages.count {
if lhs.messages[i].id != rhs.messages[i].id {
return false
}
if lhs.messages[i].stableVersion != rhs.messages[i].stableVersion {
return false
}
}
return true
}
}
private struct SparseItems: Equatable {
enum Item: Equatable {
case range(count: Int)
case anchor(id: MessageId, timestamp: Int32, message: Message?)
static func ==(lhs: Item, rhs: Item) -> Bool {
switch lhs {
case let .range(count):
if case .range(count) = rhs {
return true
} else {
return false
}
case let .anchor(lhsId, lhsTimestamp, lhsMessage):
if case let .anchor(rhsId, rhsTimestamp, rhsMessage) = rhs {
if lhsId != rhsId {
return false
}
if lhsTimestamp != rhsTimestamp {
return false
}
if let lhsMessage = lhsMessage, let rhsMessage = rhsMessage {
if lhsMessage.id != rhsMessage.id {
return false
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
} else if (lhsMessage != nil) != (rhsMessage != nil) {
return false
}
return true
} else {
return false
}
}
}
}
var items: [Item]
}
private var topSectionItemRequestCount: Int = 100
private var topSection: TopSection?
private var topItemsDisposable = MetaDisposable()
private var deletedMessagesDisposable: Disposable?
private var sparseItems: SparseItems?
private var sparseItemsDisposable: Disposable?
private struct LoadingHole: Equatable {
var anchor: MessageId
var direction: LoadHoleDirection
}
private let loadHoleDisposable = MetaDisposable()
private var loadingHole: LoadingHole?
private var loadingPlaceholders: [MessageId: Disposable] = [:]
private var loadedPlaceholders: [MessageId: Message] = [:]
let statePromise = Promise<SparseMessageList.State>()
init(queue: Queue, account: Account, peerId: PeerId, threadId: Int64?, messageTag: MessageTags) {
self.queue = queue
self.account = account
self.peerId = peerId
self.threadId = threadId
self.messageTag = messageTag
self.resetTopSection()
if self.threadId == nil {
self.sparseItemsDisposable = (self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<SparseItems, NoError> in
guard let inputPeer = inputPeer else {
return .single(SparseItems(items: []))
}
guard let messageFilter = messageFilterForTagMask(messageTag) else {
return .single(SparseItems(items: []))
}
//TODO:api
return account.network.request(Api.functions.messages.getSearchResultsPositions(flags: 0, peer: inputPeer, savedPeerId: nil, filter: messageFilter, offsetId: 0, limit: 1000))
|> map { result -> SparseItems in
switch result {
case let .searchResultsPositions(totalCount, positions):
struct Position: Equatable {
var id: Int32
var date: Int32
var offset: Int
}
var positions: [Position] = positions.map { position -> Position in
switch position {
case let .searchResultPosition(id, date, offset):
return Position(id: id, date: date, offset: Int(offset))
}
}
positions.sort(by: { lhs, rhs in
return lhs.id > rhs.id
})
var result = SparseItems(items: [])
for i in 0 ..< positions.count {
if i != 0 {
let deltaCount = positions[i].offset - 1 - positions[i - 1].offset
if deltaCount > 0 {
result.items.append(.range(count: deltaCount))
}
}
result.items.append(.anchor(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: positions[i].id), timestamp: positions[i].date, message: nil))
if i == positions.count - 1 {
let remainingCount = Int(totalCount) - 1 - positions[i].offset
if remainingCount > 0 {
result.items.append(.range(count: remainingCount))
}
}
}
return result
}
}
|> `catch` { _ -> Signal<SparseItems, NoError> in
return .single(SparseItems(items: []))
}
}
|> deliverOn(self.queue)).start(next: { [weak self] sparseItems in
guard let strongSelf = self else {
return
}
strongSelf.sparseItems = sparseItems
if strongSelf.topSection != nil {
strongSelf.updateState()
}
})
}
self.deletedMessagesDisposable = (account.postbox.combinedView(keys: [.deletedMessages(peerId: peerId)])
|> deliverOn(self.queue)).start(next: { [weak self] views in
guard let strongSelf = self else {
return
}
guard let view = views.views[.deletedMessages(peerId: peerId)] as? DeletedMessagesView else {
return
}
strongSelf.processDeletedMessages(ids: view.currentDeletedMessages)
})
}
deinit {
self.topItemsDisposable.dispose()
self.sparseItemsDisposable?.dispose()
self.loadHoleDisposable.dispose()
self.deletedMessagesDisposable?.dispose()
}
private func resetTopSection() {
let count: Int
count = 200
let location: ChatLocationInput = .peer(peerId: self.peerId, threadId: self.threadId)
self.topItemsDisposable.set((self.account.postbox.aroundMessageHistoryViewForLocation(location, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: .tag(self.messageTag), appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: [])
|> deliverOn(self.queue)).start(next: { [weak self] view, updateType, _ in
guard let strongSelf = self else {
return
}
switch updateType {
case .FillHole:
strongSelf.resetTopSection()
default:
strongSelf.updateTopSection(view: view)
}
}))
}
private func processDeletedMessages(ids: [MessageId]) {
if let sparseItems = self.sparseItems {
let idsSet = Set(ids)
var removeIndices: [Int] = []
for i in 0 ..< sparseItems.items.count {
switch sparseItems.items[i] {
case let .anchor(id, _, _):
if idsSet.contains(id) {
removeIndices.append(i)
}
default:
break
}
}
if !removeIndices.isEmpty {
for index in removeIndices.reversed() {
self.sparseItems?.items.remove(at: index)
}
self.updateState()
}
}
}
func loadMoreFromTopSection() {
self.topSectionItemRequestCount += 100
self.resetTopSection()
}
func loadHole(anchor: MessageId, direction: LoadHoleDirection, completion: @escaping () -> Void) {
guard let sparseItems = self.sparseItems else {
completion()
return
}
var loadRange: ClosedRange<Int32>?
var loadCount: Int?
centralItemSearch: for i in 0 ..< sparseItems.items.count {
switch sparseItems.items[i] {
case let .anchor(id, _, _):
if id == anchor {
func lowerStep(index: Int, holeRange: inout ClosedRange<Int32>, holeCount: inout Int) -> Bool {
switch sparseItems.items[index] {
case let .anchor(id, _, message):
holeRange = id.id ... holeRange.upperBound
holeCount += 1
if message != nil {
return false
} else {
if holeCount > 90 {
return false
}
}
case let .range(count):
if holeCount + count > 90 {
return false
}
holeCount += count
}
return true
}
func upperStep(index: Int, holeRange: inout ClosedRange<Int32>, holeCount: inout Int) -> Bool {
switch sparseItems.items[index] {
case let .anchor(id, _, message):
holeRange = holeRange.lowerBound ... id.id
holeCount += 1
if message != nil {
return false
} else {
if holeCount > 90 {
return false
}
}
case let .range(count):
if holeCount + count > 90 {
return false
}
holeCount += count
}
return true
}
var holeCount = 1
var holeRange: ClosedRange<Int32> = id.id ... id.id
var lowerIndex = i - 1
var upperIndex = i + 1
while true {
if holeCount > 90 {
break
}
if lowerIndex == -1 && upperIndex == sparseItems.items.count {
break
}
if lowerIndex >= 0 {
if !upperStep(index: lowerIndex, holeRange: &holeRange, holeCount: &holeCount) {
lowerIndex = -1
} else {
lowerIndex -= 1
if lowerIndex == -1 {
holeRange = holeRange.lowerBound ... (Int32.max - 1)
}
}
}
if upperIndex < sparseItems.items.count {
if !lowerStep(index: upperIndex, holeRange: &holeRange, holeCount: &holeCount) {
upperIndex = sparseItems.items.count
} else {
upperIndex += 1
if upperIndex == sparseItems.items.count {
holeRange = 1 ... holeRange.upperBound
}
}
}
}
loadRange = holeRange
loadCount = holeCount
break centralItemSearch
}
default:
break
}
}
guard let range = loadRange, let expectedCount = loadCount else {
completion()
return
}
let loadingHole = LoadingHole(anchor: anchor, direction: direction)
if self.loadingHole != nil {
completion()
return
}
self.loadingHole = loadingHole
let mappedDirection: MessageHistoryViewRelativeHoleDirection = .range(start: MessageId(peerId: anchor.peerId, namespace: anchor.namespace, id: range.upperBound), end: MessageId(peerId: anchor.peerId, namespace: anchor.namespace, id: range.lowerBound - 1))
let account = self.account
self.loadHoleDisposable.set((fetchMessageHistoryHole(
accountPeerId: self.account.peerId,
source: .network(self.account.network),
postbox: self.account.postbox,
peerInput: .direct(peerId: self.peerId, threadId: nil),
namespace: Namespaces.Message.Cloud,
direction: mappedDirection,
space: .tag(self.messageTag),
count: 100
)
|> mapToSignal { result -> Signal<[Message], NoError> in
guard let result = result else {
return .single([])
}
return account.postbox.transaction { transaction -> [Message] in
return result.ids.sorted(by: { $0 > $1 }).compactMap(transaction.getMessage)
}
}
|> deliverOn(self.queue)).start(next: { [weak self] messages in
guard let strongSelf = self else {
completion()
return
}
if messages.count != expectedCount {
Logger.shared.log("SparseMessageList", "unexpected message count")
}
var lowerIndex: Int?
var upperIndex: Int?
for i in 0 ..< strongSelf.sparseItems!.items.count {
switch strongSelf.sparseItems!.items[i] {
case let .anchor(id, _, _):
if id.id == range.lowerBound {
lowerIndex = i
}
if id.id == range.upperBound {
upperIndex = i
}
default:
break
}
}
if range.lowerBound <= 1 {
lowerIndex = strongSelf.sparseItems!.items.count - 1
}
if range.upperBound >= Int32.max - 1 {
upperIndex = 0
}
if let lowerIndex = lowerIndex, let upperIndex = upperIndex {
strongSelf.sparseItems!.items.removeSubrange(upperIndex ... lowerIndex)
var insertIndex = upperIndex
for message in messages.sorted(by: { $0.id > $1.id }) {
strongSelf.sparseItems!.items.insert(.anchor(id: message.id, timestamp: message.timestamp, message: message), at: insertIndex)
insertIndex += 1
}
}
let anchors = strongSelf.sparseItems!.items.compactMap { item -> MessageId? in
if case let .anchor(id, _, _) = item {
return id
} else {
return nil
}
}
assert(anchors.sorted(by: >) == anchors)
strongSelf.updateState()
if strongSelf.loadingHole == loadingHole {
strongSelf.loadingHole = nil
}
completion()
}))
/*let mappedDirection: MessageHistoryViewRelativeHoleDirection
switch direction {
case .around:
mappedDirection = .aroundId(anchor)
case .earlier:
mappedDirection = .range(start: anchor, end: MessageId(peerId: anchor.peerId, namespace: anchor.namespace, id: 1))
case .later:
mappedDirection = .range(start: anchor, end: MessageId(peerId: anchor.peerId, namespace: anchor.namespace, id: Int32.max - 1))
}
let account = self.account
self.loadHoleDisposable.set((fetchMessageHistoryHole(accountPeerId: self.account.peerId, source: .network(self.account.network), postbox: self.account.postbox, peerInput: .direct(peerId: self.peerId, threadId: nil), namespace: Namespaces.Message.Cloud, direction: mappedDirection, space: .tag(self.messageTag), count: 100)
|> mapToSignal { result -> Signal<[Message], NoError> in
guard let result = result else {
return .single([])
}
return account.postbox.transaction { transaction -> [Message] in
return result.ids.sorted(by: { $0 > $1 }).compactMap(transaction.getMessage)
}
}
|> deliverOn(self.queue)).start(next: { [weak self] messages in
guard let strongSelf = self else {
completion()
return
}
if strongSelf.sparseItems != nil {
var sparseHoles: [(itemIndex: Int, leftId: MessageId, rightId: MessageId)] = []
for i in 0 ..< strongSelf.sparseItems!.items.count {
switch strongSelf.sparseItems!.items[i] {
case let .anchor(id, timestamp, _):
for messageIndex in 0 ..< messages.count {
if messages[messageIndex].id == id {
strongSelf.sparseItems!.items[i] = .anchor(id: id, timestamp: timestamp, message: messages[messageIndex])
}
}
case .range:
if i == 0 {
assertionFailure()
} else {
var leftId: MessageId?
switch strongSelf.sparseItems!.items[i - 1] {
case .range:
assertionFailure()
case let .anchor(id, _, _):
leftId = id
}
var rightId: MessageId?
if i != strongSelf.sparseItems!.items.count - 1 {
switch strongSelf.sparseItems!.items[i + 1] {
case .range:
assertionFailure()
case let .anchor(id, _, _):
rightId = id
}
}
if let leftId = leftId, let rightId = rightId {
sparseHoles.append((itemIndex: i, leftId: leftId, rightId: rightId))
} else if let leftId = leftId, i == strongSelf.sparseItems!.items.count - 1 {
sparseHoles.append((itemIndex: i, leftId: leftId, rightId: MessageId(peerId: leftId.peerId, namespace: leftId.namespace, id: 1)))
} else {
assertionFailure()
}
}
}
}
for (itemIndex, initialLeftId, initialRightId) in sparseHoles.reversed() {
var leftCovered = false
var rightCovered = false
for message in messages {
if message.id == initialLeftId {
leftCovered = true
}
if message.id == initialRightId {
rightCovered = true
}
}
if leftCovered && rightCovered {
strongSelf.sparseItems!.items.remove(at: itemIndex)
var insertIndex = itemIndex
for message in messages {
if message.id < initialLeftId && message.id > initialRightId {
strongSelf.sparseItems!.items.insert(.anchor(id: message.id, timestamp: message.timestamp, message: message), at: insertIndex)
insertIndex += 1
}
}
} else if leftCovered {
for i in 0 ..< messages.count {
if messages[i].id == initialLeftId {
var spaceItemIndex = itemIndex
for j in i + 1 ..< messages.count {
switch strongSelf.sparseItems!.items[spaceItemIndex] {
case let .range(count):
strongSelf.sparseItems!.items[spaceItemIndex] = .range(count: count - 1)
case .anchor:
assertionFailure()
}
strongSelf.sparseItems!.items.insert(.anchor(id: messages[j].id, timestamp: messages[j].timestamp, message: messages[j]), at: spaceItemIndex)
spaceItemIndex += 1
}
switch strongSelf.sparseItems!.items[spaceItemIndex] {
case let .range(count):
if count <= 0 {
strongSelf.sparseItems!.items.remove(at: spaceItemIndex)
}
case .anchor:
assertionFailure()
}
break
}
}
} else if rightCovered {
for i in (0 ..< messages.count).reversed() {
if messages[i].id == initialRightId {
for j in (0 ..< i).reversed() {
switch strongSelf.sparseItems!.items[itemIndex] {
case let .range(count):
strongSelf.sparseItems!.items[itemIndex] = .range(count: count - 1)
case .anchor:
assertionFailure()
}
strongSelf.sparseItems!.items.insert(.anchor(id: messages[j].id, timestamp: messages[j].timestamp, message: messages[j]), at: itemIndex + 1)
}
switch strongSelf.sparseItems!.items[itemIndex] {
case let .range(count):
if count <= 0 {
strongSelf.sparseItems!.items.remove(at: itemIndex)
}
case .anchor:
assertionFailure()
}
break
}
}
}
}
strongSelf.updateState()
}
if strongSelf.loadingHole == loadingHole {
strongSelf.loadingHole = nil
}
completion()
}))*/
}
private func updateTopSection(view: MessageHistoryView) {
var topSection: TopSection?
if view.isLoading {
topSection = nil
} else {
topSection = TopSection(messages: view.entries.lazy.reversed().map { entry in
return entry.message
})
}
if self.topSection != topSection {
self.topSection = topSection
}
self.updateState()
}
private func updateState() {
var items: [SparseMessageList.State.Item] = []
var minMessageId: MessageId?
if let topSection = self.topSection {
for i in 0 ..< topSection.messages.count {
let message = topSection.messages[i]
items.append(SparseMessageList.State.Item(index: items.count, content: .message(message: message, isLocal: true)))
if let minMessageIdValue = minMessageId {
if message.id < minMessageIdValue {
minMessageId = message.id
}
} else {
minMessageId = message.id
}
}
}
let topItemCount = items.count
var totalCount = items.count
if let sparseItems = self.sparseItems {
var sparseIndex = 0
for i in 0 ..< sparseItems.items.count {
switch sparseItems.items[i] {
case let .anchor(id, timestamp, message):
if sparseIndex >= topItemCount {
if let message = message {
items.append(SparseMessageList.State.Item(index: totalCount, content: .message(message: message, isLocal: false)))
} else {
items.append(SparseMessageList.State.Item(index: totalCount, content: .placeholder(id: id, timestamp: timestamp)))
}
totalCount += 1
}
sparseIndex += 1
case let .range(count):
if sparseIndex >= topItemCount {
totalCount += count
} else {
let overflowCount = sparseIndex + count - topItemCount
if overflowCount > 0 {
totalCount += count
}
}
sparseIndex += count
}
}
}
self.statePromise.set(.single(SparseMessageList.State(
items: items,
totalCount: totalCount,
isLoading: self.topSection == nil
)))
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public struct State {
public final class Item {
public enum Content {
case message(message: Message, isLocal: Bool)
case placeholder(id: MessageId, timestamp: Int32)
}
public let index: Int
public let content: Content
init(index: Int, content: Content) {
self.index = index
self.content = content
}
}
public var items: [Item]
public var totalCount: Int
public var isLoading: Bool
}
public enum LoadHoleDirection {
case around
case earlier
case later
}
public var state: Signal<State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.statePromise.get().start(next: subscriber.putNext))
}
return disposable
}
}
init(account: Account, peerId: PeerId, threadId: Int64?, messageTag: MessageTags) {
self.queue = Queue()
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, account: account, peerId: peerId, threadId: threadId, messageTag: messageTag)
})
}
public func loadMoreFromTopSection() {
self.impl.with { impl in
impl.loadMoreFromTopSection()
}
}
public func loadHole(anchor: MessageId, direction: LoadHoleDirection, completion: @escaping () -> Void) {
self.impl.with { impl in
impl.loadHole(anchor: anchor, direction: direction, completion: completion)
}
}
}
public final class SparseMessageCalendar {
private final class Impl {
struct InternalState {
var nextRequestOffset: Int32?
var minTimestamp: Int32?
var messagesByDay: [Int32: SparseMessageCalendar.Entry]
}
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let threadId: Int64?
private let messageTag: MessageTags
private let displayMedia: Bool
private var state: InternalState
let statePromise = Promise<InternalState>()
private let disposable = MetaDisposable()
private var isLoadingMore: Bool = false {
didSet {
self.isLoadingMorePromise.set(.single(self.isLoadingMore))
}
}
private let isLoadingMorePromise = Promise<Bool>(false)
var isLoadingMoreSignal: Signal<Bool, NoError> {
return self.isLoadingMorePromise.get()
}
init(queue: Queue, account: Account, peerId: PeerId, threadId: Int64?, messageTag: MessageTags, displayMedia: Bool) {
self.queue = queue
self.account = account
self.peerId = peerId
self.threadId = threadId
self.messageTag = messageTag
self.displayMedia = displayMedia
self.state = InternalState(nextRequestOffset: 0, minTimestamp: nil, messagesByDay: [:])
self.statePromise.set(.single(self.state))
self.maybeLoadMore()
}
deinit {
self.disposable.dispose()
}
func maybeLoadMore() {
if self.isLoadingMore {
return
}
self.loadMore()
}
func removeMessagesInRange(minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType, completion: @escaping () -> Void) -> Disposable {
var removeKeys: [Int32] = []
for (id, entry) in self.state.messagesByDay {
if entry.message.timestamp >= minTimestamp && entry.message.timestamp <= maxTimestamp {
removeKeys.append(id)
}
}
for id in removeKeys {
self.state.messagesByDay.removeValue(forKey: id)
}
self.statePromise.set(.single(self.state))
return _internal_clearHistoryInRangeInteractively(postbox: self.account.postbox, peerId: self.peerId, threadId: self.threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type).start(completed: {
completion()
})
}
private func loadMore() {
guard let nextRequestOffset = self.state.nextRequestOffset else {
return
}
if self.threadId != nil {
return
}
if !self.displayMedia {
return
}
self.isLoadingMore = true
struct LoadResult {
var messagesByDay: [Int32: SparseMessageCalendar.Entry]
var nextOffset: Int32?
var minMessageId: MessageId?
var minTimestamp: Int32?
}
let account = self.account
let accountPeerId = account.peerId
let peerId = self.peerId
let messageTag = self.messageTag
self.disposable.set((self.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> mapToSignal { peer -> Signal<LoadResult, NoError> in
guard let peer = peer else {
return .single(LoadResult(messagesByDay: [:], nextOffset: nil, minMessageId: nil, minTimestamp: nil))
}
guard let inputPeer = apiInputPeer(peer) else {
return .single(LoadResult(messagesByDay: [:], nextOffset: nil, minMessageId: nil, minTimestamp: nil))
}
guard let messageFilter = messageFilterForTagMask(messageTag) else {
return .single(LoadResult(messagesByDay: [:], nextOffset: nil, minMessageId: nil, minTimestamp: nil))
}
//TODO:api
return self.account.network.request(Api.functions.messages.getSearchResultsCalendar(flags: 0, peer: inputPeer, savedPeerId: nil, filter: messageFilter, offsetId: nextRequestOffset, offsetDate: 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.SearchResultsCalendar?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<LoadResult, NoError> in
return account.postbox.transaction { transaction -> LoadResult in
guard let result = result else {
return LoadResult(messagesByDay: [:], nextOffset: nil, minMessageId: nil, minTimestamp: nil)
}
switch result {
case let .searchResultsCalendar(_, _, minDate, minMsgId, _, periods, messages, chats, users):
var parsedMessages: [StoreMessage] = []
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
for message in messages {
if let parsedMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum) {
parsedMessages.append(parsedMessage)
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let _ = transaction.addMessages(parsedMessages, location: .Random)
var minMessageId: Int32?
var messagesByDay: [Int32: SparseMessageCalendar.Entry] = [:]
for period in periods {
switch period {
case let .searchResultsCalendarPeriod(date, minMsgId, _, count):
if let message = transaction.getMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: minMsgId)) {
messagesByDay[date] = SparseMessageCalendar.Entry(message: message, count: Int(count))
}
if let minMessageIdValue = minMessageId {
if minMsgId < minMessageIdValue {
minMessageId = minMsgId
}
} else {
minMessageId = minMsgId
}
}
}
return LoadResult(messagesByDay: messagesByDay, nextOffset: minMessageId, minMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: minMsgId), minTimestamp: minDate)
}
}
}
}
|> deliverOn(self.queue)).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
if let minTimestamp = result.minTimestamp {
strongSelf.state.minTimestamp = minTimestamp
}
strongSelf.state.nextRequestOffset = result.nextOffset
for (timestamp, entry) in result.messagesByDay {
strongSelf.state.messagesByDay[timestamp] = entry
}
strongSelf.statePromise.set(.single(strongSelf.state))
strongSelf.isLoadingMore = false
}))
}
}
public struct Entry {
public var message: Message
public var count: Int
}
public struct State {
public var messagesByDay: [Int32: Entry]
public var minTimestamp: Int32?
public var hasMore: Bool
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public var minTimestamp: Int32?
private var disposable: Disposable?
init(account: Account, peerId: PeerId, threadId: Int64?, messageTag: MessageTags, displayMedia: Bool) {
let queue = Queue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, account: account, peerId: peerId, threadId: threadId, messageTag: messageTag, displayMedia: displayMedia)
})
self.disposable = self.state.start(next: { [weak self] state in
self?.minTimestamp = state.minTimestamp
})
}
deinit {
self.disposable?.dispose()
}
public var state: Signal<State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.statePromise.get().start(next: { state in
subscriber.putNext(State(
messagesByDay: state.messagesByDay,
minTimestamp: state.minTimestamp,
hasMore: state.nextRequestOffset != nil
))
}))
}
return disposable
}
}
public var isLoadingMore: Signal<Bool, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.isLoadingMoreSignal.start(next: subscriber.putNext))
}
return disposable
}
}
public func loadMore() {
self.impl.with { impl in
impl.maybeLoadMore()
}
}
public func removeMessagesInRange(minTimestamp: Int32, maxTimestamp: Int32, type: InteractiveHistoryClearingType, completion: @escaping () -> Void) -> Disposable {
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.removeMessagesInRange(minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type, completion: completion))
}
return disposable
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,106 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public final class TimeZoneList: Codable, Equatable {
public final class Item: Codable, Equatable {
public let id: String
public let title: String
public let utcOffset: Int32
public init(id: String, title: String, utcOffset: Int32) {
self.id = id
self.title = title
self.utcOffset = utcOffset
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs === rhs {
return true
}
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.utcOffset != rhs.utcOffset {
return false
}
return true
}
}
public let items: [Item]
public let hashValue: Int32
public init(items: [Item], hashValue: Int32) {
self.items = items
self.hashValue = hashValue
}
public static func ==(lhs: TimeZoneList, rhs: TimeZoneList) -> Bool {
if lhs === rhs {
return true
}
if lhs.items != rhs.items {
return false
}
if lhs.hashValue != rhs.hashValue {
return false
}
return true
}
}
func _internal_cachedTimeZoneList(account: Account) -> Signal<TimeZoneList?, NoError> {
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.timezoneList()]))
return account.postbox.combinedView(keys: [viewKey])
|> map { views -> TimeZoneList? in
guard let view = views.views[viewKey] as? PreferencesView else {
return nil
}
guard let value = view.values[PreferencesKeys.timezoneList()]?.get(TimeZoneList.self) else {
return nil
}
return value
}
}
func _internal_keepCachedTimeZoneListUpdated(account: Account) -> Signal<Never, NoError> {
let updateSignal = _internal_cachedTimeZoneList(account: account)
|> take(1)
|> mapToSignal { list -> Signal<Never, NoError> in
return account.network.request(Api.functions.help.getTimezonesList(hash: list?.hashValue ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.help.TimezonesList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
guard let result else {
return .complete()
}
return account.postbox.transaction { transaction in
switch result {
case let .timezonesList(timezones, hash):
var items: [TimeZoneList.Item] = []
for item in timezones {
switch item {
case let .timezone(id, name, utcOffset):
items.append(TimeZoneList.Item(id: id, title: name, utcOffset: utcOffset))
}
}
transaction.setPreferencesEntry(key: PreferencesKeys.timezoneList(), value: PreferencesEntry(TimeZoneList(items: items, hashValue: hash)))
case .timezonesListNotModified:
break
}
}
|> ignoreValues
}
}
return updateSignal
}
@@ -0,0 +1,80 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
public enum RequestUpdateTodoMessageError {
case generic
}
func _internal_requestUpdateTodoMessageItems(account: Account, messageId: MessageId, completedIds: [Int32], incompletedIds: [Int32]) -> Signal<Never, RequestUpdateTodoMessageError> {
return account.postbox.transaction { transaction -> Signal<Never, RequestUpdateTodoMessageError> in
guard let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) else {
return .complete()
}
var completedBy = account.peerId
if messageId.peerId.namespace == Namespaces.Peer.CloudChannel, let cachedChannelData = transaction.getPeerCachedData(peerId: messageId.peerId) as? CachedChannelData, let sendAsPeerId = cachedChannelData.sendAsPeerId {
completedBy = sendAsPeerId
}
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var media: [Media] = []
if let todo = currentMessage.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var updatedCompletions = todo.completions
for id in completedIds {
updatedCompletions.append(TelegramMediaTodo.Completion(id: id, date: timestamp, completedBy: completedBy))
}
updatedCompletions.removeAll(where: { incompletedIds.contains($0.id) })
media = [todo.withUpdated(completions: updatedCompletions)]
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media))
})
return account.network.request(Api.functions.messages.toggleTodoCompleted(peer: inputPeer, msgId: messageId.id, completed: completedIds, incompleted: incompletedIds))
|> mapError { _ -> RequestUpdateTodoMessageError in
return .generic
}
|> map { result in
account.stateManager.addUpdates(result)
}
|> ignoreValues
}
|> castError(RequestUpdateTodoMessageError.self)
|> switchToLatest
|> ignoreValues
}
public enum AppendTodoMessageError {
case generic
}
func _internal_appendTodoMessageItems(account: Account, messageId: MessageId, items: [TelegramMediaTodo.Item]) -> Signal<Never, AppendTodoMessageError> {
return account.postbox.transaction { transaction -> Signal<Never, AppendTodoMessageError> in
guard let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) else {
return .complete()
}
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var media: [Media] = []
if let todo = currentMessage.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo {
var updatedItems = todo.items
updatedItems.append(contentsOf: items)
media = [todo.withUpdated(items: updatedItems)]
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media))
})
return account.network.request(Api.functions.messages.appendTodoList(peer: inputPeer, msgId: messageId.id, list: items.map { $0.apiItem }))
|> mapError { _ -> AppendTodoMessageError in
return .generic
}
|> map { result in
account.stateManager.addUpdates(result)
}
|> ignoreValues
}
|> castError(AppendTodoMessageError.self)
|> switchToLatest
|> ignoreValues
}
@@ -0,0 +1,175 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum EngineAudioTranscriptionResult {
case success
case error
}
private enum InternalAudioTranscriptionResult {
case success(Api.messages.TranscribedAudio)
case error(AudioTranscriptionMessageAttribute.TranscriptionError)
case limitExceeded(Int32)
}
func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal<EngineAudioTranscriptionResult, NoError> {
return postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<EngineAudioTranscriptionResult, NoError> in
guard let inputPeer = inputPeer else {
return .single(.error)
}
return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id))
|> map { result -> InternalAudioTranscriptionResult in
return .success(result)
}
|> `catch` { error -> Signal<InternalAudioTranscriptionResult, NoError> in
let mappedError: AudioTranscriptionMessageAttribute.TranscriptionError
if error.errorDescription.hasPrefix("FLOOD_WAIT_") {
if let range = error.errorDescription.range(of: "_", options: .backwards) {
if let value = Int32(error.errorDescription[range.upperBound...]) {
return .single(.limitExceeded(value))
}
}
mappedError = .generic
} else if error.errorDescription == "MSG_VOICE_TOO_LONG" {
mappedError = .tooLong
} else {
mappedError = .generic
}
return .single(.error(mappedError))
}
|> mapToSignal { result -> Signal<EngineAudioTranscriptionResult, NoError> in
return postbox.transaction { transaction -> EngineAudioTranscriptionResult in
let updatedAttribute: AudioTranscriptionMessageAttribute
switch result {
case let .success(transcribedAudio):
switch transcribedAudio {
case let .transcribedAudio(flags, transcriptionId, text, trialRemainingCount, trialUntilDate):
let isPending = (flags & (1 << 0)) != 0
updatedAttribute = AudioTranscriptionMessageAttribute(id: transcriptionId, text: text, isPending: isPending, didRate: false, error: nil)
_internal_updateAudioTranscriptionTrialState(transaction: transaction) { current in
var updated = current
if let trialRemainingCount = trialRemainingCount, trialRemainingCount > 0 {
updated = updated.withUpdatedRemainingCount(trialRemainingCount)
} else if let trialUntilDate = trialUntilDate {
updated = updated.withUpdatedCooldownUntilTime(trialUntilDate)
} else {
updated = updated.withUpdatedCooldownUntilTime(nil)
}
return updated
}
}
case let .error(error):
updatedAttribute = AudioTranscriptionMessageAttribute(id: 0, text: "", isPending: false, didRate: false, error: error)
case let .limitExceeded(timeout):
let cooldownTime = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + timeout
_internal_updateAudioTranscriptionTrialState(transaction: transaction) { current in
var updated = current
updated = updated.withUpdatedCooldownUntilTime(cooldownTime)
return updated
}
return .error
}
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is AudioTranscriptionMessageAttribute) }
attributes.append(updatedAttribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
if updatedAttribute.error == nil {
return .success
} else {
return .error
}
}
}
}
}
func _internal_rateAudioTranscription(postbox: Postbox, network: Network, messageId: MessageId, id: Int64, isGood: Bool) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Api.InputPeer? in
transaction.updateMessage(messageId, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var attributes = currentMessage.attributes
for i in 0 ..< attributes.count {
if let attribute = attributes[i] as? AudioTranscriptionMessageAttribute {
attributes[i] = attribute.withDidRate()
}
}
return .update(StoreMessage(
id: currentMessage.id,
customStableId: nil,
globallyUniqueId: currentMessage.globallyUniqueId,
groupingKey: currentMessage.groupingKey,
threadId: currentMessage.threadId,
timestamp: currentMessage.timestamp,
flags: StoreMessageFlags(currentMessage.flags),
tags: currentMessage.tags,
globalTags: currentMessage.globalTags,
localTags: currentMessage.localTags,
forwardInfo: storeForwardInfo,
authorId: currentMessage.author?.id,
text: currentMessage.text,
attributes: attributes,
media: currentMessage.media
))
})
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer = inputPeer else {
return .complete()
}
return network.request(Api.functions.messages.rateTranscribedAudio(peer: inputPeer, msgId: messageId.id, transcriptionId: id, good: isGood ? .boolTrue : .boolFalse))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
}
public enum AudioTranscription {
public struct TrialState: Equatable, Codable {
public let cooldownUntilTime: Int32?
public let remainingCount: Int32
func withUpdatedCooldownUntilTime(_ time: Int32?) -> AudioTranscription.TrialState {
return AudioTranscription.TrialState(cooldownUntilTime: time, remainingCount: time != nil ? 0 : max(1, self.remainingCount))
}
func withUpdatedRemainingCount(_ remainingCount: Int32) -> AudioTranscription.TrialState {
return AudioTranscription.TrialState(remainingCount: remainingCount)
}
public init(cooldownUntilTime: Int32? = nil, remainingCount: Int32) {
self.cooldownUntilTime = cooldownUntilTime
self.remainingCount = remainingCount
}
public static var defaultValue: AudioTranscription.TrialState {
return AudioTranscription.TrialState(
cooldownUntilTime: nil,
remainingCount: 1
)
}
}
}
func _internal_updateAudioTranscriptionTrialState(transaction: Transaction, _ f: (AudioTranscription.TrialState) -> AudioTranscription.TrialState) {
let current = transaction.getPreferencesEntry(key: PreferencesKeys.audioTranscriptionTrialState)?.get(AudioTranscription.TrialState.self) ?? .defaultValue
transaction.setPreferencesEntry(key: PreferencesKeys.audioTranscriptionTrialState, value: PreferencesEntry(f(current)))
}
@@ -0,0 +1,336 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum TranslationError {
case generic
case invalidMessageId
case textIsEmpty
case textTooLong
case invalidLanguage
case limitExceeded
case tryAlternative
}
func _internal_translate(network: Network, text: String, toLang: String, entities: [MessageTextEntity] = []) -> Signal<(String, [MessageTextEntity])?, TranslationError> {
var flags: Int32 = 0
flags |= (1 << 1)
return network.request(Api.functions.messages.translateText(flags: flags, peer: nil, id: nil, text: [.textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()))], toLang: toLang))
|> mapError { error -> TranslationError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "MSG_ID_INVALID" {
return .invalidMessageId
} else if error.errorDescription == "INPUT_TEXT_EMPTY" {
return .textIsEmpty
} else if error.errorDescription == "INPUT_TEXT_TOO_LONG" {
return .textTooLong
} else if error.errorDescription == "TO_LANG_INVALID" {
return .invalidLanguage
} else if error.errorDescription == "TRANSLATIONS_DISABLED_ALT" {
return .tryAlternative
} else {
return .generic
}
}
|> mapToSignal { result -> Signal<(String, [MessageTextEntity])?, TranslationError> in
switch result {
case let .translateResult(results):
if case let .textWithEntities(text, entities) = results.first {
return .single((text, messageTextEntitiesFromApiEntities(entities)))
} else {
return .single(nil)
}
}
}
}
func _internal_translateTexts(network: Network, texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> {
var flags: Int32 = 0
flags |= (1 << 1)
var apiTexts: [Api.TextWithEntities] = []
for text in texts {
apiTexts.append(.textWithEntities(text: text.0, entities: apiEntitiesFromMessageTextEntities(text.1, associatedPeers: SimpleDictionary())))
}
return network.request(Api.functions.messages.translateText(flags: flags, peer: nil, id: nil, text: apiTexts, toLang: toLang))
|> mapError { error -> TranslationError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "MSG_ID_INVALID" {
return .invalidMessageId
} else if error.errorDescription == "INPUT_TEXT_EMPTY" {
return .textIsEmpty
} else if error.errorDescription == "INPUT_TEXT_TOO_LONG" {
return .textTooLong
} else if error.errorDescription == "TO_LANG_INVALID" {
return .invalidLanguage
} else {
return .generic
}
}
|> mapToSignal { result -> Signal<[(String, [MessageTextEntity])], TranslationError> in
var texts: [(String, [MessageTextEntity])] = []
switch result {
case let .translateResult(results):
for result in results {
if case let .textWithEntities(text, entities) = result {
texts.append((text, messageTextEntitiesFromApiEntities(entities)))
}
}
}
return .single(texts)
}
}
func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal<Never, TranslationError> {
var signals: [Signal<Void, TranslationError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
signals.append(_internal_translateMessagesByPeerId(account: account, peerId: peerId, messageIds: messageIds, fromLang: fromLang, toLang: toLang, enableLocalIfPossible: enableLocalIfPossible))
}
return combineLatest(signals)
|> ignoreValues
}
public protocol ExperimentalInternalTranslationService: AnyObject {
func translate(texts: [AnyHashable: String], fromLang: String, toLang: String) -> Signal<[AnyHashable: String]?, NoError>
}
public var engineExperimentalInternalTranslationService: ExperimentalInternalTranslationService?
private func _internal_translateMessagesByPeerId(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal<Void, TranslationError> {
return account.postbox.transaction { transaction -> (Api.InputPeer?, [Message]) in
return (transaction.getPeer(peerId).flatMap(apiInputPeer), messageIds.compactMap({ transaction.getMessage($0) }))
}
|> castError(TranslationError.self)
|> mapToSignal { (inputPeer, messages) -> Signal<Void, TranslationError> in
guard let inputPeer = inputPeer else {
return .never()
}
let polls = messages.compactMap { message in
if let poll = message.media.first as? TelegramMediaPoll {
return (poll, message.id)
} else {
return nil
}
}
let pollSignals = polls.map { (poll, id) in
var texts: [(String, [MessageTextEntity])] = []
texts.append((poll.text, poll.textEntities))
for option in poll.options {
texts.append((option.text, option.entities))
}
if let solution = poll.results.solution {
texts.append((solution.text, solution.entities))
}
return _internal_translateTexts(network: account.network, texts: texts, toLang: toLang)
}
let audioTranscriptions = messages.compactMap { message in
if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending {
return (audioTranscription.text, message.id)
} else {
return nil
}
}
let audioTranscriptionsSignals = audioTranscriptions.map { (text, id) in
return _internal_translate(network: account.network, text: text, toLang: toLang)
}
var flags: Int32 = 0
flags |= (1 << 0)
let id: [Int32] = messageIds.map { $0.id }
let msgs: Signal<Api.messages.TranslatedText?, TranslationError>
if id.isEmpty {
msgs = .single(nil)
} else {
if enableLocalIfPossible, let engineExperimentalInternalTranslationService, let fromLang {
msgs = account.postbox.transaction { transaction -> [MessageId: String] in
var texts: [MessageId: String] = [:]
for messageId in messageIds {
if let message = transaction.getMessage(messageId) {
texts[message.id] = message.text
}
}
return texts
}
|> castError(TranslationError.self)
|> mapToSignal { messageTexts -> Signal<Api.messages.TranslatedText?, TranslationError> in
var mappedTexts: [AnyHashable: String] = [:]
for (id, text) in messageTexts {
mappedTexts[AnyHashable(id)] = text
}
return engineExperimentalInternalTranslationService.translate(texts: mappedTexts, fromLang: fromLang, toLang: toLang)
|> castError(TranslationError.self)
|> mapToSignal { resultTexts -> Signal<Api.messages.TranslatedText?, TranslationError> in
guard let resultTexts else {
return .fail(.generic)
}
var result: [Api.TextWithEntities] = []
for messageId in messageIds {
if let text = resultTexts[AnyHashable(messageId)] {
result.append(.textWithEntities(text: text, entities: []))
} else if let text = messageTexts[messageId] {
result.append(.textWithEntities(text: text, entities: []))
} else {
result.append(.textWithEntities(text: "", entities: []))
}
}
return .single(.translateResult(result: result))
}
}
} else {
msgs = account.network.request(Api.functions.messages.translateText(flags: flags, peer: inputPeer, id: id, text: nil, toLang: toLang))
|> map(Optional.init)
|> mapError { error -> TranslationError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else if error.errorDescription == "MSG_ID_INVALID" {
return .invalidMessageId
} else if error.errorDescription == "INPUT_TEXT_EMPTY" {
return .textIsEmpty
} else if error.errorDescription == "INPUT_TEXT_TOO_LONG" {
return .textTooLong
} else if error.errorDescription == "TO_LANG_INVALID" {
return .invalidLanguage
} else {
return .generic
}
}
}
}
return combineLatest(msgs, combineLatest(pollSignals), combineLatest(audioTranscriptionsSignals))
|> mapToSignal { (result, pollResults, audioTranscriptionsResults) -> Signal<Void, TranslationError> in
return account.postbox.transaction { transaction in
if case let .translateResult(results) = result {
var index = 0
for result in results {
let messageId = messageIds[index]
if case let .textWithEntities(text, entities) = result {
let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: text, entities: messageTextEntitiesFromApiEntities(entities), toLang: toLang)
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
attributes.append(updatedAttribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
index += 1
}
}
if !pollResults.isEmpty {
for (i, poll) in polls.enumerated() {
let result = pollResults[i]
if !result.isEmpty {
transaction.updateMessage(poll.1, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
var attrOptions: [TranslationMessageAttribute.Additional] = []
for (i, _) in poll.0.options.enumerated() {
var translated = result.count > i + 1 ? result[i + 1] : (poll.0.options[i].text, poll.0.options[i].entities)
if translated.0.isEmpty {
translated = (poll.0.options[i].text, poll.0.options[i].entities)
}
attrOptions.append(TranslationMessageAttribute.Additional(text: translated.0, entities: translated.1))
}
let solution: TranslationMessageAttribute.Additional?
if result.count > 1 + poll.0.options.count, !result[result.count - 1].0.isEmpty {
solution = TranslationMessageAttribute.Additional(text: result[result.count - 1].0, entities: result[result.count - 1].1)
} else {
solution = nil
}
let title = result[0].0.isEmpty ? (poll.0.text, poll.0.textEntities) : result[0]
let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: title.0, entities: title.1, additional: attrOptions, pollSolution: solution, toLang: toLang)
attributes.append(updatedAttribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
}
if !audioTranscriptionsResults.isEmpty {
for (i, audioTranscription) in audioTranscriptions.enumerated() {
if let result = audioTranscriptionsResults[i] {
transaction.updateMessage(audioTranscription.1, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: result.0, entities: result.1, additional: [], pollSolution: nil, toLang: toLang)
attributes.append(updatedAttribute)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
}
}
|> castError(TranslationError.self)
}
}
}
func _internal_togglePeerMessagesTranslationHidden(account: Account, peerId: EnginePeer.Id, hidden: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
if let cachedData = cachedData as? CachedUserData {
var updatedFlags = cachedData.flags
if hidden {
updatedFlags.insert(.translationHidden)
} else {
updatedFlags.remove(.translationHidden)
}
return cachedData.withUpdatedFlags(updatedFlags)
} else if let cachedData = cachedData as? CachedGroupData {
var updatedFlags = cachedData.flags
if hidden {
updatedFlags.insert(.translationHidden)
} else {
updatedFlags.remove(.translationHidden)
}
return cachedData.withUpdatedFlags(updatedFlags)
} else if let cachedData = cachedData as? CachedChannelData {
var updatedFlags = cachedData.flags
if hidden {
updatedFlags.insert(.translationHidden)
} else {
updatedFlags.remove(.translationHidden)
}
return cachedData.withUpdatedFlags(updatedFlags)
} else {
return cachedData
}
})
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer = inputPeer else {
return .never()
}
var flags: Int32 = 0
if hidden {
flags |= (1 << 0)
}
return account.network.request(Api.functions.messages.togglePeerTranslations(flags: flags, peer: inputPeer))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Bool?, NoError> in
return .single(nil)
}
|> ignoreValues
}
}
@@ -0,0 +1,223 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum UpdatePinnedMessageError {
case generic
}
public enum PinnedMessageUpdate {
case pin(id: MessageId, silent: Bool, forThisPeerOnlyIfPossible: Bool)
case clear(id: MessageId)
}
func _internal_requestUpdatePinnedMessage(account: Account, peerId: PeerId, update: PinnedMessageUpdate) -> Signal<Void, UpdatePinnedMessageError> {
return account.postbox.transaction { transaction -> (Peer?, CachedPeerData?) in
return (transaction.getPeer(peerId), transaction.getPeerCachedData(peerId: peerId))
}
|> mapError { _ -> UpdatePinnedMessageError in
}
|> mapToSignal { peer, cachedPeerData -> Signal<Void, UpdatePinnedMessageError> in
guard let peer = peer, let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
if let channel = peer as? TelegramChannel {
let canManagePin = channel.hasPermission(.pinMessages)
if !canManagePin {
return .fail(.generic)
}
} else if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
break
default:
if let defaultBannedRights = group.defaultBannedRights {
if defaultBannedRights.flags.contains(.banPinMessages) {
return .fail(.generic)
}
}
}
} else if let _ = peer as? TelegramUser, let cachedPeerData = cachedPeerData as? CachedUserData {
if !cachedPeerData.canPinMessages {
return .fail(.generic)
}
}
var flags: Int32 = 0
let messageId: Int32
switch update {
case let .pin(id, silent, forThisPeerOnlyIfPossible):
messageId = id.id
if silent {
flags |= (1 << 0)
}
if forThisPeerOnlyIfPossible {
flags |= (1 << 2)
}
case let .clear(id):
messageId = id.id
flags |= 1 << 1
}
let request = Api.functions.messages.updatePinnedMessage(flags: flags, peer: inputPeer, id: messageId)
return account.network.request(request)
|> mapError { _ -> UpdatePinnedMessageError in
return .generic
}
|> mapToSignal { updates -> Signal<Void, UpdatePinnedMessageError> in
account.stateManager.addUpdates(updates)
return account.postbox.transaction { transaction in
switch updates {
case let .updates(updates, _, _, _, _):
if updates.isEmpty {
if peerId.namespace == Namespaces.Peer.CloudChannel {
let messageId: MessageId
switch update {
case let .pin(id, _, _):
messageId = id
case let .clear(id):
messageId = id
}
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var updatedTags = currentMessage.tags
switch update {
case .pin:
updatedTags.insert(.pinned)
case .clear:
updatedTags.remove(.pinned)
}
if updatedTags == currentMessage.tags {
return .skip
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
})
}
}
default:
break
}
}
|> mapError { _ -> UpdatePinnedMessageError in
}
}
}
}
func _internal_requestUnpinAllMessages(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<Never, UpdatePinnedMessageError> {
return account.postbox.transaction { transaction -> (Peer?, Peer?, CachedPeerData?) in
let peer = transaction.getPeer(peerId)
var subPeer: Peer?
if let channel = peer as? TelegramChannel, channel.isMonoForum, let threadId {
subPeer = transaction.getPeer(PeerId(threadId))
}
return (peer, subPeer, transaction.getPeerCachedData(peerId: peerId))
}
|> mapError { _ -> UpdatePinnedMessageError in
}
|> mapToSignal { peer, subPeer, cachedPeerData -> Signal<Never, UpdatePinnedMessageError> in
guard let peer = peer, let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
if let channel = peer as? TelegramChannel {
let canManagePin = channel.hasPermission(.pinMessages)
if !canManagePin {
return .fail(.generic)
}
} else if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
break
default:
if let defaultBannedRights = group.defaultBannedRights {
if defaultBannedRights.flags.contains(.banPinMessages) {
return .fail(.generic)
}
}
}
} else if let _ = peer as? TelegramUser, let cachedPeerData = cachedPeerData as? CachedUserData {
if !cachedPeerData.canPinMessages {
return .fail(.generic)
}
}
enum InternalError {
case error(String)
case restart
}
var flags: Int32 = 0
var topMsgId: Int32?
var savedPeerId: Api.InputPeer?
if let threadId {
if let channel = peer as? TelegramChannel, channel.isMonoForum {
if let inputSubPeer = subPeer.flatMap(apiInputPeer) {
flags |= (1 << 1)
savedPeerId = inputSubPeer
}
} else {
flags |= (1 << 0)
topMsgId = Int32(clamping: threadId)
}
}
let request: Signal<Never, InternalError> = account.network.request(Api.functions.messages.unpinAllMessages(flags: flags, peer: inputPeer, topMsgId: topMsgId, savedPeerId: savedPeerId))
|> mapError { error -> InternalError in
return .error(error.errorDescription)
}
|> mapToSignal { result -> Signal<Bool, InternalError> in
switch result {
case let .affectedHistory(_, _, count):
if count != 0 {
return .fail(.restart)
}
}
return .single(true)
}
|> retry(retryOnError: { error -> Bool in
switch error {
case .restart:
return true
default:
return false
}
}, delayIncrement: 0.0, maxDelay: 0.0, maxRetries: 100, onQueue: .concurrentDefaultQueue())
|> mapToSignal { _ -> Signal<Never, InternalError> in
let signal: Signal<Never, InternalError> = account.postbox.transaction { transaction -> Void in
for index in transaction.getMessageIndicesWithTag(peerId: peerId, threadId: nil, namespace: Namespaces.Message.Cloud, tag: .pinned) {
transaction.updateMessage(index.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(forwardInfo)
}
var tags = currentMessage.tags
tags.remove(.pinned)
if tags == currentMessage.tags {
return .skip
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
})
}
}
|> castError(InternalError.self)
|> ignoreValues
return signal
}
return request
|> mapError { _ -> UpdatePinnedMessageError in
return .generic
}
}
}
@@ -0,0 +1,51 @@
import Foundation
import SwiftSignalKit
import Postbox
public extension TelegramEngine {
final class Notices {
private let account: Account
init(account: Account) {
self.account = account
}
public func set<T: Codable>(id: NoticeEntryKey, item: T?) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
if let item = item, let entry = CodableEntry(item) {
transaction.setNoticeEntry(key: id, value: entry)
} else {
transaction.setNoticeEntry(key: id, value: nil)
}
}
|> ignoreValues
}
public func getServerProvidedSuggestions(reload: Bool = false) -> Signal<[ServerProvidedSuggestion], NoError> {
if reload {
return _internal_fetchPromoInfo(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network)
|> mapToSignal {
return _internal_getServerProvidedSuggestions(account: self.account)
}
} else {
return _internal_getServerProvidedSuggestions(account: self.account)
}
}
public func getServerDismissedSuggestions() -> Signal<[String], NoError> {
return _internal_getServerDismissedSuggestions(account: self.account)
}
public func dismissServerProvidedSuggestion(suggestion: String) -> Signal<Never, NoError> {
return _internal_dismissServerProvidedSuggestion(account: self.account, suggestion: suggestion)
}
public func getPeerSpecificServerProvidedSuggestions(peerId: EnginePeer.Id) -> Signal<[PeerSpecificServerProvidedSuggestion], NoError> {
return _internal_getPeerSpecificServerProvidedSuggestions(postbox: self.account.postbox, peerId: peerId)
}
public func dismissPeerSpecificServerProvidedSuggestion(peerId: PeerId, suggestion: PeerSpecificServerProvidedSuggestion) -> Signal<Never, NoError> {
return _internal_dismissPeerSpecificServerProvidedSuggestion(account: self.account, peerId: peerId, suggestion: suggestion)
}
}
}
@@ -0,0 +1,36 @@
import Foundation
import SwiftSignalKit
import Postbox
public extension TelegramEngine {
final class OrderedLists {
private let account: Account
init(account: Account) {
self.account = account
}
public func addOrMoveToFirstPosition<T: Codable>(collectionId: Int32, id: MemoryBuffer, item: T, removeTailIfCountExceeds: Int?) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
if let entry = CodableEntry(item) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: collectionId, item: OrderedItemListEntry(id: id, contents: entry), removeTailIfCountExceeds: removeTailIfCountExceeds)
}
}
|> ignoreValues
}
public func clear(collectionId: Int32) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
transaction.replaceOrderedItemListItems(collectionId: collectionId, items: [])
}
|> ignoreValues
}
public func removeItem(collectionId: Int32, id: MemoryBuffer) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
transaction.removeOrderedItemListItem(collectionId: collectionId, itemId: id)
}
|> ignoreValues
}
}
}
@@ -0,0 +1,225 @@
import Foundation
import Postbox
import MtProtoKit
import SwiftSignalKit
import TelegramApi
public enum AssignAppStoreTransactionError {
case generic
case timeout
case serverProvided
}
public enum AppStoreTransactionPurpose {
case subscription
case upgrade
case restore
case gift(peerId: EnginePeer.Id, currency: String, amount: Int64)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64, text: String?, entities: [MessageTextEntity]?)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case stars(count: Int64, currency: String, amount: Int64, peerId: EnginePeer.Id?)
case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64)
case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, users: Int32)
case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String, currency: String, amount: Int64)
}
private func apiInputStorePaymentPurpose(postbox: Postbox, purpose: AppStoreTransactionPurpose) -> Signal<Api.InputStorePaymentPurpose, NoError> {
switch purpose {
case .subscription, .upgrade, .restore:
var flags: Int32 = 0
switch purpose {
case .upgrade:
flags |= (1 << 1)
case .restore:
flags |= (1 << 0)
default:
break
}
return .single(.inputStorePaymentPremiumSubscription(flags: flags))
case let .gift(peerId, currency, amount):
return postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer -> Signal<Api.InputStorePaymentPurpose, NoError> in
guard let inputUser = apiInputUser(peer) else {
return .complete()
}
return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount))
}
case let .giftCode(peerIds, boostPeerId, currency, amount, text, entities):
return postbox.transaction { transaction -> Api.InputStorePaymentPurpose in
var flags: Int32 = 0
var apiBoostPeer: Api.InputPeer?
var apiInputUsers: [Api.InputUser] = []
for peerId in peerIds {
if let user = transaction.getPeer(peerId), let apiUser = apiInputUser(user) {
apiInputUsers.append(apiUser)
}
}
if let boostPeerId = boostPeerId, let boostPeer = transaction.getPeer(boostPeerId), let apiPeer = apiInputPeer(boostPeer) {
apiBoostPeer = apiPeer
flags |= (1 << 0)
}
var message: Api.TextWithEntities?
if let text, !text.isEmpty {
flags |= (1 << 1)
message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? [])
}
return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount, message: message)
}
case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount):
return postbox.transaction { transaction -> Signal<Api.InputStorePaymentPurpose, NoError> in
guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else {
return .complete()
}
var flags: Int32 = 0
if onlyNewSubscribers {
flags |= (1 << 0)
}
if showWinners {
flags |= (1 << 3)
}
var additionalPeers: [Api.InputPeer] = []
if !additionalPeerIds.isEmpty {
flags |= (1 << 1)
for peerId in additionalPeerIds {
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
additionalPeers.append(inputPeer)
}
}
}
if !countries.isEmpty {
flags |= (1 << 2)
}
if let _ = prizeDescription {
flags |= (1 << 4)
}
return .single(.inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount))
}
|> switchToLatest
case let .stars(count, currency, amount, peerId):
let peerSignal: Signal<Api.InputPeer?, NoError>
if let peerId {
peerSignal = postbox.loadedPeerWithId(peerId)
|> map { peer in
return apiInputPeer(peer)
}
} else {
peerSignal = .single(nil)
}
return peerSignal
|> map { spendPurposePeer in
var flags: Int32 = 0
if let _ = spendPurposePeer {
flags |= (1 << 0)
}
return .inputStorePaymentStarsTopup(flags: flags, stars: count, currency: currency, amount: amount, spendPurposePeer: spendPurposePeer)
}
case let .starsGift(peerId, count, currency, amount):
return postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer -> Signal<Api.InputStorePaymentPurpose, NoError> in
guard let inputUser = apiInputUser(peer) else {
return .complete()
}
return .single(.inputStorePaymentStarsGift(userId: inputUser, stars: count, currency: currency, amount: amount))
}
case let .starsGiveaway(stars, boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount, users):
return postbox.transaction { transaction -> Signal<Api.InputStorePaymentPurpose, NoError> in
guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else {
return .complete()
}
var flags: Int32 = 0
if onlyNewSubscribers {
flags |= (1 << 0)
}
if showWinners {
flags |= (1 << 3)
}
var additionalPeers: [Api.InputPeer] = []
if !additionalPeerIds.isEmpty {
flags |= (1 << 1)
for peerId in additionalPeerIds {
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
additionalPeers.append(inputPeer)
}
}
}
if !countries.isEmpty {
flags |= (1 << 2)
}
if let _ = prizeDescription {
flags |= (1 << 4)
}
return .single(.inputStorePaymentStarsGiveaway(flags: flags, stars: stars, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users))
}
|> switchToLatest
case let .authCode(restore, phoneNumber, phoneCodeHash, currency, amount):
var flags: Int32 = 0
if restore {
flags |= (1 << 0)
}
return .single(.inputStorePaymentAuthCode(flags: flags, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, currency: currency, amount: amount))
}
}
func _internal_sendAppStoreReceipt(postbox: Postbox, network: Network, stateManager: AccountStateManager, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal<Never, AssignAppStoreTransactionError> {
return apiInputStorePaymentPurpose(postbox: postbox, purpose: purpose)
|> castError(AssignAppStoreTransactionError.self)
|> mapToSignal { purpose -> Signal<Never, AssignAppStoreTransactionError> in
return network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose))
|> mapError { error -> AssignAppStoreTransactionError in
if error.errorCode == 406 {
return .serverProvided
} else {
return .generic
}
}
|> mapToSignal { updates -> Signal<Never, AssignAppStoreTransactionError> in
stateManager.addUpdates(updates)
return .complete()
}
}
}
func _internal_sendAppStoreReceipt(postbox: Postbox, network: Network, stateManager: UnauthorizedAccountStateManager, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal<Never, AssignAppStoreTransactionError> {
return apiInputStorePaymentPurpose(postbox: postbox, purpose: purpose)
|> castError(AssignAppStoreTransactionError.self)
|> mapToSignal { purpose -> Signal<Never, AssignAppStoreTransactionError> in
return network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose))
|> mapError { error -> AssignAppStoreTransactionError in
if error.errorCode == 406 {
return .serverProvided
} else {
return .generic
}
}
|> mapToSignal { updates -> Signal<Never, AssignAppStoreTransactionError> in
stateManager.addUpdates(updates)
return .complete()
}
}
}
public enum RestoreAppStoreReceiptError {
case generic
}
func _internal_canPurchasePremium(postbox: Postbox, network: Network, purpose: AppStoreTransactionPurpose) -> Signal<Bool, NoError> {
return apiInputStorePaymentPurpose(postbox: postbox, purpose: purpose)
|> mapToSignal { purpose -> Signal<Bool, NoError> in
return network.request(Api.functions.payments.canPurchaseStore(purpose: purpose))
|> map { result -> Bool in
switch result {
case .boolTrue:
return true
case .boolFalse:
return false
}
}
|> `catch` { _ -> Signal<Bool, NoError> in
return .single(false)
}
}
}
@@ -0,0 +1,58 @@
import Foundation
import Postbox
import TelegramApi
import MtProtoKit
import SwiftSignalKit
func _internal_getBankCardInfo(account: Account, cardNumber: String) -> Signal<BankCardInfo?, NoError> {
return currentWebDocumentsHostDatacenterId(postbox: account.postbox, isTestingEnvironment: false)
|> mapToSignal { datacenterId in
let signal: Signal<Api.payments.BankCardData, MTRpcError>
if account.network.datacenterId != datacenterId {
signal = account.network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|> castError(MTRpcError.self)
|> mapToSignal { worker in
return worker.request(Api.functions.payments.getBankCardData(number: cardNumber))
}
} else {
signal = account.network.request(Api.functions.payments.getBankCardData(number: cardNumber))
}
return signal
|> map { result -> BankCardInfo? in
return BankCardInfo(apiBankCardData: result)
}
|> `catch` { _ -> Signal<BankCardInfo?, NoError> in
return .single(nil)
}
}
}
public struct BankCardUrl {
public let title: String
public let url: String
}
public struct BankCardInfo {
public let title: String
public let urls: [BankCardUrl]
}
extension BankCardUrl {
init(apiBankCardOpenUrl: Api.BankCardOpenUrl) {
switch apiBankCardOpenUrl {
case let .bankCardOpenUrl(url, name):
self.title = name
self.url = url
}
}
}
extension BankCardInfo {
init(apiBankCardData: Api.payments.BankCardData) {
switch apiBankCardData {
case let .bankCardData(title, urls):
self.title = title
self.urls = urls.map { BankCardUrl(apiBankCardOpenUrl: $0) }
}
}
}
@@ -0,0 +1,967 @@
import Foundation
import Postbox
import MtProtoKit
import SwiftSignalKit
import TelegramApi
public enum BotPaymentInvoiceSource {
case message(MessageId)
case slug(String)
case premiumGiveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, option: PremiumGiftCodeOption)
case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption, text: String?, entities: [MessageTextEntity]?)
case stars(option: StarsTopUpOption, peerId: EnginePeer.Id?)
case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64)
case starsChatSubscription(hash: String)
case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, users: Int32)
case starGift(hideName: Bool, includeUpgrade: Bool, peerId: EnginePeer.Id, giftId: Int64, text: String?, entities: [MessageTextEntity]?)
case starGiftUpgrade(keepOriginalInfo: Bool, reference: StarGiftReference)
case starGiftTransfer(reference: StarGiftReference, toPeerId: EnginePeer.Id)
case premiumGift(peerId: EnginePeer.Id, option: CachedPremiumGiftOption, text: String?, entities: [MessageTextEntity]?)
case starGiftResale(slug: String, toPeerId: EnginePeer.Id, ton: Bool)
case starGiftPrepaidUpgrade(peerId: EnginePeer.Id, hash: String)
case starGiftDropOriginalDetails(reference: StarGiftReference)
case starGiftAuctionBid(update: Bool, hideName: Bool, peerId: EnginePeer.Id?, giftId: Int64, bidAmount: Int64, text: String?, entities: [MessageTextEntity]?)
}
public struct BotPaymentInvoiceFields: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let name = BotPaymentInvoiceFields(rawValue: 1 << 0)
public static let phone = BotPaymentInvoiceFields(rawValue: 1 << 1)
public static let email = BotPaymentInvoiceFields(rawValue: 1 << 2)
public static let shippingAddress = BotPaymentInvoiceFields(rawValue: 1 << 3)
public static let flexibleShipping = BotPaymentInvoiceFields(rawValue: 1 << 4)
public static let phoneAvailableToProvider = BotPaymentInvoiceFields(rawValue: 1 << 5)
public static let emailAvailableToProvider = BotPaymentInvoiceFields(rawValue: 1 << 6)
}
public struct BotPaymentPrice : Equatable {
public let label: String
public let amount: Int64
public init(label: String, amount: Int64) {
self.label = label
self.amount = amount
}
}
public struct BotPaymentInvoice : Equatable {
public struct Tip: Equatable {
public var max: Int64
public var suggested: [Int64]
}
public struct RecurrentInfo: Equatable {
public var termsUrl: String
public var isRecurrent: Bool
}
public let isTest: Bool
public let requestedFields: BotPaymentInvoiceFields
public let currency: String
public let prices: [BotPaymentPrice]
public let tip: Tip?
public let termsInfo: RecurrentInfo?
public let subscriptionPeriod: Int32?
public init(isTest: Bool, requestedFields: BotPaymentInvoiceFields, currency: String, prices: [BotPaymentPrice], tip: Tip?, termsInfo: RecurrentInfo?, subscriptionPeriod: Int32?) {
self.isTest = isTest
self.requestedFields = requestedFields
self.currency = currency
self.prices = prices
self.tip = tip
self.termsInfo = termsInfo
self.subscriptionPeriod = subscriptionPeriod
}
}
public struct BotPaymentNativeProvider : Equatable {
public let name: String
public let params: String
}
public struct BotPaymentShippingAddress: Equatable {
public let streetLine1: String
public let streetLine2: String
public let city: String
public let state: String
public let countryIso2: String
public let postCode: String
public init(streetLine1: String, streetLine2: String, city: String, state: String, countryIso2: String, postCode: String) {
self.streetLine1 = streetLine1
self.streetLine2 = streetLine2
self.city = city
self.state = state
self.countryIso2 = countryIso2
self.postCode = postCode
}
}
public struct BotPaymentRequestedInfo: Equatable {
public var name: String?
public var phone: String?
public var email: String?
public var shippingAddress: BotPaymentShippingAddress?
public init(name: String?, phone: String?, email: String?, shippingAddress: BotPaymentShippingAddress?) {
self.name = name
self.phone = phone
self.email = email
self.shippingAddress = shippingAddress
}
}
public enum BotPaymentSavedCredentials: Equatable {
case card(id: String, title: String)
public static func ==(lhs: BotPaymentSavedCredentials, rhs: BotPaymentSavedCredentials) -> Bool {
switch lhs {
case let .card(id, title):
if case .card(id, title) = rhs {
return true
} else {
return false
}
}
}
}
public struct BotPaymentForm : Equatable {
public let id: Int64
public let canSaveCredentials: Bool
public let passwordMissing: Bool
public let invoice: BotPaymentInvoice
public let paymentBotId: PeerId?
public let providerId: PeerId?
public let url: String?
public let nativeProvider: BotPaymentNativeProvider?
public let savedInfo: BotPaymentRequestedInfo?
public let savedCredentials: [BotPaymentSavedCredentials]
public let additionalPaymentMethods: [BotPaymentMethod]
public init(id: Int64, canSaveCredentials: Bool, passwordMissing: Bool, invoice: BotPaymentInvoice, paymentBotId: PeerId?, providerId: PeerId?, url: String?, nativeProvider: BotPaymentNativeProvider?, savedInfo: BotPaymentRequestedInfo?, savedCredentials: [BotPaymentSavedCredentials], additionalPaymentMethods: [BotPaymentMethod]) {
self.id = id
self.canSaveCredentials = canSaveCredentials
self.passwordMissing = passwordMissing
self.invoice = invoice
self.paymentBotId = paymentBotId
self.providerId = providerId
self.url = url
self.nativeProvider = nativeProvider
self.savedInfo = savedInfo
self.savedCredentials = savedCredentials
self.additionalPaymentMethods = additionalPaymentMethods
}
}
public struct BotPaymentMethod: Equatable {
public let url: String
public let title: String
}
extension BotPaymentMethod {
init(apiPaymentFormMethod: Api.PaymentFormMethod) {
switch apiPaymentFormMethod {
case let .paymentFormMethod(url, title):
self.init(url: url, title: title)
}
}
}
public enum BotPaymentFormRequestError {
case generic
case alreadyActive
case noPaymentNeeded
case disallowedStarGift
case starGiftResellTooEarly(Int32)
case starGiftUserLimit
}
extension BotPaymentInvoice {
init(apiInvoice: Api.Invoice) {
switch apiInvoice {
case let .invoice(flags, currency, prices, maxTipAmount, suggestedTipAmounts, termsUrl, subscriptionPeriod):
var fields = BotPaymentInvoiceFields()
if (flags & (1 << 1)) != 0 {
fields.insert(.name)
}
if (flags & (1 << 2)) != 0 {
fields.insert(.phone)
}
if (flags & (1 << 3)) != 0 {
fields.insert(.email)
}
if (flags & (1 << 4)) != 0 {
fields.insert(.shippingAddress)
}
if (flags & (1 << 5)) != 0 {
fields.insert(.flexibleShipping)
}
if (flags & (1 << 6)) != 0 {
fields.insert(.phoneAvailableToProvider)
}
if (flags & (1 << 7)) != 0 {
fields.insert(.emailAvailableToProvider)
}
let isRecurrent = (flags & (1 << 9)) != 0
var termsInfo: BotPaymentInvoice.RecurrentInfo?
if let termsUrl = termsUrl {
termsInfo = BotPaymentInvoice.RecurrentInfo(termsUrl: termsUrl, isRecurrent: isRecurrent)
}
var parsedTip: BotPaymentInvoice.Tip?
if let maxTipAmount = maxTipAmount, let suggestedTipAmounts = suggestedTipAmounts {
parsedTip = BotPaymentInvoice.Tip(max: maxTipAmount, suggested: suggestedTipAmounts)
}
self.init(isTest: (flags & (1 << 0)) != 0, requestedFields: fields, currency: currency, prices: prices.map {
switch $0 {
case let .labeledPrice(label, amount):
return BotPaymentPrice(label: label, amount: amount)
}
}, tip: parsedTip, termsInfo: termsInfo, subscriptionPeriod: subscriptionPeriod)
}
}
}
extension BotPaymentRequestedInfo {
init(apiInfo: Api.PaymentRequestedInfo) {
switch apiInfo {
case let .paymentRequestedInfo(_, name, phone, email, shippingAddress):
var parsedShippingAddress: BotPaymentShippingAddress?
if let shippingAddress = shippingAddress {
switch shippingAddress {
case let .postAddress(streetLine1, streetLine2, city, state, countryIso2, postCode):
parsedShippingAddress = BotPaymentShippingAddress(streetLine1: streetLine1, streetLine2: streetLine2, city: city, state: state, countryIso2: countryIso2, postCode: postCode)
}
}
self.init(name: name, phone: phone, email: email, shippingAddress: parsedShippingAddress)
}
}
}
func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInvoiceSource) -> Api.InputInvoice? {
switch source {
case let .message(messageId):
guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else {
return nil
}
return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id)
case let .slug(slug):
return .inputInvoiceSlug(slug: slug)
case let .premiumGiveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount, option):
guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else {
return nil
}
var flags: Int32 = 0
if onlyNewSubscribers {
flags |= (1 << 0)
}
if showWinners {
flags |= (1 << 3)
}
var additionalPeers: [Api.InputPeer] = []
if !additionalPeerIds.isEmpty {
flags |= (1 << 1)
for peerId in additionalPeerIds {
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
additionalPeers.append(inputPeer)
}
}
}
if !countries.isEmpty {
flags |= (1 << 2)
}
if let _ = prizeDescription {
flags |= (1 << 4)
}
let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
flags = 0
if let _ = option.storeProductId {
flags |= (1 << 0)
}
if option.storeQuantity > 0 {
flags |= (1 << 1)
}
let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: option.users, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount)
return .inputInvoicePremiumGiftCode(purpose: inputPurpose, option: option)
case let .giftCode(users, currency, amount, option, text, entities):
var inputUsers: [Api.InputUser] = []
if !users.isEmpty {
for peerId in users {
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputUser(peer) {
inputUsers.append(inputPeer)
}
}
}
var inputPurposeFlags: Int32 = 0
var message: Api.TextWithEntities?
if let text, !text.isEmpty {
inputPurposeFlags |= (1 << 1)
message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? [])
}
let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: inputPurposeFlags, users: inputUsers, boostPeer: nil, currency: currency, amount: amount, message: message)
var flags: Int32 = 0
if let _ = option.storeProductId {
flags |= (1 << 0)
}
if option.storeQuantity > 0 {
flags |= (1 << 1)
}
let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: option.users, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount)
return .inputInvoicePremiumGiftCode(purpose: inputPurpose, option: option)
case let .stars(option, peerId):
var flags: Int32 = 0
var spendPurposePeer: Api.InputPeer?
if let peerId, let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
flags |= (1 << 0)
spendPurposePeer = inputPeer
}
return .inputInvoiceStars(purpose: .inputStorePaymentStarsTopup(flags: flags, stars: option.count, currency: option.currency, amount: option.amount, spendPurposePeer: spendPurposePeer))
case let .starsGift(peerId, count, currency, amount):
guard let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) else {
return nil
}
return .inputInvoiceStars(purpose: .inputStorePaymentStarsGift(userId: inputUser, stars: count, currency: currency, amount: amount))
case let .starsChatSubscription(hash):
return .inputInvoiceChatInviteSubscription(hash: hash)
case let .starsGiveaway(stars, boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount, users):
guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else {
return nil
}
var flags: Int32 = 0
if onlyNewSubscribers {
flags |= (1 << 0)
}
if showWinners {
flags |= (1 << 3)
}
var additionalPeers: [Api.InputPeer] = []
if !additionalPeerIds.isEmpty {
flags |= (1 << 1)
for peerId in additionalPeerIds {
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
additionalPeers.append(inputPeer)
}
}
}
if !countries.isEmpty {
flags |= (1 << 2)
}
if let _ = prizeDescription {
flags |= (1 << 4)
}
return .inputInvoiceStars(purpose: .inputStorePaymentStarsGiveaway(flags: flags, stars: stars, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users))
case let .starGift(hideName, includeUpgrade, peerId, giftId, text, entities):
guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else {
return nil
}
var flags: Int32 = 0
if hideName {
flags |= (1 << 0)
}
if includeUpgrade {
flags |= (1 << 2)
}
var message: Api.TextWithEntities?
if let text, !text.isEmpty {
flags |= (1 << 1)
message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? [])
}
return .inputInvoiceStarGift(flags: flags, peer: inputPeer, giftId: giftId, message: message)
case let .starGiftUpgrade(keepOriginalInfo, reference):
var flags: Int32 = 0
if keepOriginalInfo {
flags |= (1 << 0)
}
return reference.apiStarGiftReference(transaction: transaction).flatMap { .inputInvoiceStarGiftUpgrade(flags: flags, stargift: $0) }
case let .starGiftTransfer(reference, toPeerId):
guard let peer = transaction.getPeer(toPeerId), let inputPeer = apiInputPeer(peer) else {
return nil
}
return reference.apiStarGiftReference(transaction: transaction).flatMap { .inputInvoiceStarGiftTransfer(stargift: $0, toId: inputPeer) }
case let .premiumGift(peerId, option, text, entities):
guard let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) else {
return nil
}
var flags: Int32 = 0
var message: Api.TextWithEntities?
if let text, !text.isEmpty {
flags |= (1 << 0)
message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? [])
}
return .inputInvoicePremiumGiftStars(flags: flags, userId: inputUser, months: option.months, message: message)
case let .starGiftResale(slug, toPeerId, ton):
guard let peer = transaction.getPeer(toPeerId), let inputPeer = apiInputPeer(peer) else {
return nil
}
var flags: Int32 = 0
if ton {
flags |= 1 << 0
}
return .inputInvoiceStarGiftResale(flags: flags, slug: slug, toId: inputPeer)
case let .starGiftPrepaidUpgrade(peerId, hash):
guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else {
return nil
}
return .inputInvoiceStarGiftPrepaidUpgrade(peer: inputPeer, hash: hash)
case let .starGiftDropOriginalDetails(reference):
return reference.apiStarGiftReference(transaction: transaction).flatMap { .inputInvoiceStarGiftDropOriginalDetails(stargift: $0) }
case let .starGiftAuctionBid(update, hideName, peerId, giftId, bidAmount, text, entities):
var flags: Int32 = 0
var inputPeer: Api.InputPeer?
var message: Api.TextWithEntities?
if update {
flags |= (1 << 2)
}
if let peerId {
guard let peer = transaction.getPeer(peerId).flatMap(apiInputPeer) else {
return nil
}
flags |= (1 << 3)
inputPeer = peer
if hideName {
flags |= (1 << 0)
}
if let text, !text.isEmpty {
flags |= (1 << 1)
message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? [])
}
}
return .inputInvoiceStarGiftAuctionBid(flags: flags, peer: inputPeer, giftId: giftId, bidAmount: bidAmount, message: message)
}
}
func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source: BotPaymentInvoiceSource) -> Signal<TelegramMediaInvoice, BotPaymentFormRequestError> {
return postbox.transaction { transaction -> Api.InputInvoice? in
return _internal_parseInputInvoice(transaction: transaction, source: source)
}
|> castError(BotPaymentFormRequestError.self)
|> mapToSignal { invoice -> Signal<TelegramMediaInvoice, BotPaymentFormRequestError> in
guard let invoice = invoice else {
return .fail(.generic)
}
let flags: Int32 = 0
return network.request(Api.functions.payments.getPaymentForm(flags: flags, invoice: invoice, themeParams: nil))
|> `catch` { error -> Signal<Api.payments.PaymentForm, BotPaymentFormRequestError> in
if error.errorDescription == "SUBSCRIPTION_ALREADY_ACTIVE" {
return .fail(.alreadyActive)
} else {
return .fail(.generic)
}
}
|> mapToSignal { result -> Signal<TelegramMediaInvoice, BotPaymentFormRequestError> in
return postbox.transaction { transaction -> TelegramMediaInvoice in
switch result {
case let .paymentForm(_, _, _, title, description, photo, invoice, _, _, _, _, _, _, _, _):
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
var parsedFlags = TelegramMediaInvoiceFlags()
if parsedInvoice.isTest {
parsedFlags.insert(.isTest)
}
if parsedInvoice.requestedFields.contains(.shippingAddress) {
parsedFlags.insert(.shippingAddressRequested)
}
return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: 0, startParam: "", extendedMedia: nil, subscriptionPeriod: parsedInvoice.subscriptionPeriod, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion)
case let .paymentFormStars(_, _, _, title, description, photo, invoice, _):
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: parsedInvoice.prices.reduce(0, { $0 + $1.amount }), startParam: "", extendedMedia: nil, subscriptionPeriod: parsedInvoice.subscriptionPeriod, flags: [], version: TelegramMediaInvoice.lastVersion)
case let .paymentFormStarGift(_, invoice):
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
return TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: parsedInvoice.prices.reduce(0, { $0 + $1.amount }), startParam: "", extendedMedia: nil, subscriptionPeriod: parsedInvoice.subscriptionPeriod, flags: [], version: TelegramMediaInvoice.lastVersion)
}
}
|> mapError { _ -> BotPaymentFormRequestError in }
}
}
}
func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, network: Network, source: BotPaymentInvoiceSource, themeParams: [String: Any]?) -> Signal<BotPaymentForm, BotPaymentFormRequestError> {
return postbox.transaction { transaction -> Api.InputInvoice? in
return _internal_parseInputInvoice(transaction: transaction, source: source)
}
|> castError(BotPaymentFormRequestError.self)
|> mapToSignal { invoice -> Signal<BotPaymentForm, BotPaymentFormRequestError> in
guard let invoice = invoice else {
return .fail(.generic)
}
var flags: Int32 = 0
var serializedThemeParams: Api.DataJSON?
if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) {
serializedThemeParams = Api.DataJSON.dataJSON(data: dataString)
}
if serializedThemeParams != nil {
flags |= 1 << 0
}
return network.request(Api.functions.payments.getPaymentForm(flags: flags, invoice: invoice, themeParams: serializedThemeParams))
|> `catch` { error -> Signal<Api.payments.PaymentForm, BotPaymentFormRequestError> in
if error.errorDescription == "NO_PAYMENT_NEEDED" {
return .fail(.noPaymentNeeded)
} else if error.errorDescription == "USER_DISALLOWED_STARGIFTS" {
return .fail(.disallowedStarGift)
} else if error.errorDescription.hasPrefix("STARGIFT_RESELL_TOO_EARLY_") {
let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "STARGIFT_RESELL_TOO_EARLY_".count)...])
if let value = Int32(timeout) {
return .fail(.starGiftResellTooEarly(value))
}
} else if error.errorDescription == "STARGIFT_USER_USAGE_LIMITED" {
return .fail(.starGiftUserLimit)
}
return .fail(.generic)
}
|> mapToSignal { result -> Signal<BotPaymentForm, BotPaymentFormRequestError> in
return postbox.transaction { transaction -> BotPaymentForm in
switch result {
case let .paymentForm(flags, id, botId, title, description, photo, invoice, providerId, url, nativeProvider, nativeParams, additionalMethods, savedInfo, savedCredentials, apiUsers):
let _ = title
let _ = description
let _ = photo
let parsedPeers = AccumulatedPeers(users: apiUsers)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
var parsedNativeProvider: BotPaymentNativeProvider?
if let nativeProvider = nativeProvider, let nativeParams = nativeParams {
switch nativeParams {
case let .dataJSON(data):
parsedNativeProvider = BotPaymentNativeProvider(name: nativeProvider, params: data)
}
}
let parsedSavedInfo = savedInfo.flatMap(BotPaymentRequestedInfo.init)
let parsedSavedCredentials = savedCredentials?.map({ savedCredentials -> BotPaymentSavedCredentials in
switch savedCredentials {
case let .paymentSavedCredentialsCard(id, title):
return .card(id: id, title: title)
}
}) ?? []
let additionalPaymentMethods = additionalMethods?.map({ BotPaymentMethod(apiPaymentFormMethod: $0) }) ?? []
return BotPaymentForm(id: id, canSaveCredentials: (flags & (1 << 2)) != 0, passwordMissing: (flags & (1 << 3)) != 0, invoice: parsedInvoice, paymentBotId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), providerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(providerId)), url: url, nativeProvider: parsedNativeProvider, savedInfo: parsedSavedInfo, savedCredentials: parsedSavedCredentials, additionalPaymentMethods: additionalPaymentMethods)
case let .paymentFormStars(flags, id, botId, title, description, photo, invoice, apiUsers):
let _ = flags
let _ = title
let _ = description
let _ = photo
let parsedPeers = AccumulatedPeers(users: apiUsers)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
return BotPaymentForm(id: id, canSaveCredentials: false, passwordMissing: false, invoice: parsedInvoice, paymentBotId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), providerId: nil, url: nil, nativeProvider: nil, savedInfo: nil, savedCredentials: [], additionalPaymentMethods: [])
case let .paymentFormStarGift(id, invoice):
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
return BotPaymentForm(id: id, canSaveCredentials: false, passwordMissing: false, invoice: parsedInvoice, paymentBotId: nil, providerId: nil, url: nil, nativeProvider: nil, savedInfo: nil, savedCredentials: [], additionalPaymentMethods: [])
}
}
|> mapError { _ -> BotPaymentFormRequestError in }
}
}
}
public enum ValidateBotPaymentFormError {
case generic
case shippingNotAvailable
case addressStateInvalid
case addressPostcodeInvalid
case addressCityInvalid
case nameInvalid
case emailInvalid
case phoneInvalid
}
public struct BotPaymentShippingOption : Equatable {
public let id: String
public let title: String
public let prices: [BotPaymentPrice]
}
public struct BotPaymentValidatedFormInfo : Equatable {
public let id: String?
public let shippingOptions: [BotPaymentShippingOption]?
}
extension BotPaymentShippingOption {
init(apiOption: Api.ShippingOption) {
switch apiOption {
case let .shippingOption(id, title, prices):
self.init(id: id, title: title, prices: prices.map {
switch $0 {
case let .labeledPrice(label, amount):
return BotPaymentPrice(label: label, amount: amount)
}
})
}
}
}
func _internal_validateBotPaymentForm(account: Account, saveInfo: Bool, source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo) -> Signal<BotPaymentValidatedFormInfo, ValidateBotPaymentFormError> {
return account.postbox.transaction { transaction -> Api.InputInvoice? in
return _internal_parseInputInvoice(transaction: transaction, source: source)
}
|> castError(ValidateBotPaymentFormError.self)
|> mapToSignal { invoice -> Signal<BotPaymentValidatedFormInfo, ValidateBotPaymentFormError> in
guard let invoice = invoice else {
return .fail(.generic)
}
var flags: Int32 = 0
if saveInfo {
flags |= (1 << 0)
}
var infoFlags: Int32 = 0
if let _ = formInfo.name {
infoFlags |= (1 << 0)
}
if let _ = formInfo.phone {
infoFlags |= (1 << 1)
}
if let _ = formInfo.email {
infoFlags |= (1 << 2)
}
var apiShippingAddress: Api.PostAddress?
if let address = formInfo.shippingAddress {
infoFlags |= (1 << 3)
apiShippingAddress = .postAddress(streetLine1: address.streetLine1, streetLine2: address.streetLine2, city: address.city, state: address.state, countryIso2: address.countryIso2, postCode: address.postCode)
}
return account.network.request(Api.functions.payments.validateRequestedInfo(flags: flags, invoice: invoice, info: .paymentRequestedInfo(flags: infoFlags, name: formInfo.name, phone: formInfo.phone, email: formInfo.email, shippingAddress: apiShippingAddress)))
|> mapError { error -> ValidateBotPaymentFormError in
if error.errorDescription == "SHIPPING_NOT_AVAILABLE" {
return .shippingNotAvailable
} else if error.errorDescription == "ADDRESS_STATE_INVALID" {
return .addressStateInvalid
} else if error.errorDescription == "ADDRESS_POSTCODE_INVALID" {
return .addressPostcodeInvalid
} else if error.errorDescription == "ADDRESS_CITY_INVALID" {
return .addressCityInvalid
} else if error.errorDescription == "REQ_INFO_NAME_INVALID" {
return .nameInvalid
} else if error.errorDescription == "REQ_INFO_EMAIL_INVALID" {
return .emailInvalid
} else if error.errorDescription == "REQ_INFO_PHONE_INVALID" {
return .phoneInvalid
} else {
return .generic
}
}
|> map { result -> BotPaymentValidatedFormInfo in
switch result {
case let .validatedRequestedInfo(_, id, shippingOptions):
return BotPaymentValidatedFormInfo(id: id, shippingOptions: shippingOptions.flatMap {
return $0.map(BotPaymentShippingOption.init)
})
}
}
}
}
public enum BotPaymentCredentials {
case generic(data: String, saveOnServer: Bool)
case saved(id: String, tempPassword: Data)
case applePay(data: String)
}
public enum SendBotPaymentFormError {
case generic
case precheckoutFailed
case paymentFailed
case alreadyPaid
case starGiftOutOfStock
case disallowedStarGift
case starGiftUserLimit
case serverProvided(String)
}
public enum SendBotPaymentResult {
case done(receiptMessageId: MessageId?, subscriptionPeerId: PeerId?, uniqueStarGift: ProfileGiftsContext.State.StarGift?)
case externalVerificationRequired(url: String)
}
func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPaymentInvoiceSource, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal<SendBotPaymentResult, SendBotPaymentFormError> {
return account.postbox.transaction { transaction -> Api.InputInvoice? in
return _internal_parseInputInvoice(transaction: transaction, source: source)
}
|> castError(SendBotPaymentFormError.self)
|> mapToSignal { invoice -> Signal<SendBotPaymentResult, SendBotPaymentFormError> in
guard let invoice = invoice else {
return .fail(.generic)
}
let apiCredentials: Api.InputPaymentCredentials
switch credentials {
case let .generic(data, saveOnServer):
var credentialsFlags: Int32 = 0
if saveOnServer {
credentialsFlags |= (1 << 0)
}
apiCredentials = .inputPaymentCredentials(flags: credentialsFlags, data: .dataJSON(data: data))
case let .saved(id, tempPassword):
apiCredentials = .inputPaymentCredentialsSaved(id: id, tmpPassword: Buffer(data: tempPassword))
case let .applePay(data):
apiCredentials = .inputPaymentCredentialsApplePay(paymentData: .dataJSON(data: data))
}
var flags: Int32 = 0
if validatedInfoId != nil {
flags |= (1 << 0)
}
if shippingOptionId != nil {
flags |= (1 << 1)
}
if tipAmount != nil {
flags |= (1 << 2)
}
return account.network.request(Api.functions.payments.sendPaymentForm(flags: flags, formId: formId, invoice: invoice, requestedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, credentials: apiCredentials, tipAmount: tipAmount))
|> map { result -> SendBotPaymentResult in
switch result {
case let .paymentResult(updates):
account.stateManager.addUpdates(updates)
var receiptMessageId: MessageId?
switch source {
case .starsChatSubscription:
let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) }
if let first = chats.first {
return .done(receiptMessageId: nil, subscriptionPeerId: first.id, uniqueStarGift: nil)
}
default:
break
}
for apiMessage in updates.messages {
if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) {
for media in message.media {
if let action = media as? TelegramMediaAction {
if case .paymentSent = action.action {
switch source {
case let .slug(slug):
for media in message.media {
if let action = media as? TelegramMediaAction, case let .paymentSent(_, _, invoiceSlug?, _, _) = action.action, invoiceSlug == slug {
if case let .Id(id) = message.id {
receiptMessageId = id
}
}
}
case let .message(messageId):
for attribute in message.attributes {
if let reply = attribute as? ReplyMessageAttribute {
if reply.messageId == messageId {
if case let .Id(id) = message.id {
receiptMessageId = id
}
}
}
}
case let .premiumGiveaway(_, _, _, _, _, _, randomId, _, _, _, _):
if message.globallyUniqueId == randomId {
if case let .Id(id) = message.id {
receiptMessageId = id
}
}
case let .starsGiveaway(_, _, _, _, _, _, _, randomId, _, _, _, _):
if message.globallyUniqueId == randomId {
if case let .Id(id) = message.id {
receiptMessageId = id
}
}
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale, .starGiftPrepaidUpgrade, .starGiftDropOriginalDetails, .starGiftAuctionBid:
receiptMessageId = nil
}
}
}
}
}
}
return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil, uniqueStarGift: nil)
case let .paymentVerificationNeeded(url):
return .externalVerificationRequired(url: url)
}
}
|> `catch` { error -> Signal<SendBotPaymentResult, SendBotPaymentFormError> in
if error.errorDescription == "BOT_PRECHECKOUT_FAILED" {
return .fail(.precheckoutFailed)
} else if error.errorDescription == "PAYMENT_FAILED" {
return .fail(.paymentFailed)
} else if error.errorDescription == "INVOICE_ALREADY_PAID" {
return .fail(.alreadyPaid)
}
return .fail(.generic)
}
}
}
public struct BotPaymentReceipt : Equatable {
public let invoice: BotPaymentInvoice
public let date: Int32
public let info: BotPaymentRequestedInfo?
public let shippingOption: BotPaymentShippingOption?
public let credentialsTitle: String
public let invoiceMedia: TelegramMediaInvoice
public let tipAmount: Int64?
public let botPaymentId: PeerId
public let transactionId: String?
public static func ==(lhs: BotPaymentReceipt, rhs: BotPaymentReceipt) -> Bool {
if lhs.invoice != rhs.invoice {
return false
}
if lhs.date != rhs.date {
return false
}
if lhs.info != rhs.info {
return false
}
if lhs.shippingOption != rhs.shippingOption {
return false
}
if lhs.credentialsTitle != rhs.credentialsTitle {
return false
}
if !lhs.invoiceMedia.isEqual(to: rhs.invoiceMedia) {
return false
}
if lhs.tipAmount != rhs.tipAmount {
return false
}
if lhs.botPaymentId != rhs.botPaymentId {
return false
}
if lhs.transactionId != rhs.transactionId {
return false
}
return true
}
}
public enum RequestBotPaymentReceiptError {
case generic
}
func _internal_requestBotPaymentReceipt(account: Account, messageId: MessageId) -> Signal<BotPaymentReceipt, RequestBotPaymentReceiptError> {
let accountPeerId = account.peerId
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> castError(RequestBotPaymentReceiptError.self)
|> mapToSignal { inputPeer -> Signal<BotPaymentReceipt, RequestBotPaymentReceiptError> in
guard let inputPeer = inputPeer else {
return .fail(.generic)
}
return account.network.request(Api.functions.payments.getPaymentReceipt(peer: inputPeer, msgId: messageId.id))
|> mapError { _ -> RequestBotPaymentReceiptError in
return .generic
}
|> mapToSignal { result -> Signal<BotPaymentReceipt, RequestBotPaymentReceiptError> in
return account.postbox.transaction { transaction -> BotPaymentReceipt in
switch result {
case let .paymentReceipt(_, date, botId, _, title, description, photo, invoice, info, shipping, tipAmount, currency, totalAmount, credentialsTitle, users):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [], users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
let parsedInfo = info.flatMap(BotPaymentRequestedInfo.init)
let shippingOption = shipping.flatMap(BotPaymentShippingOption.init)
let invoiceMedia = TelegramMediaInvoice(
title: title,
description: description,
photo: photo.flatMap(TelegramMediaWebFile.init),
receiptMessageId: nil,
currency: currency,
totalAmount: totalAmount,
startParam: "",
extendedMedia: nil,
subscriptionPeriod: parsedInvoice.subscriptionPeriod,
flags: [],
version: TelegramMediaInvoice.lastVersion
)
let botPaymentId = PeerId.init(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))
return BotPaymentReceipt(invoice: parsedInvoice, date: date, info: parsedInfo, shippingOption: shippingOption, credentialsTitle: credentialsTitle, invoiceMedia: invoiceMedia, tipAmount: tipAmount, botPaymentId: botPaymentId, transactionId: nil)
case let .paymentReceiptStars(_, date, botId, title, description, photo, invoice, currency, totalAmount, transactionId, users):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [], users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice)
let invoiceMedia = TelegramMediaInvoice(
title: title,
description: description,
photo: photo.flatMap(TelegramMediaWebFile.init),
receiptMessageId: nil,
currency: currency,
totalAmount: totalAmount,
startParam: "",
extendedMedia: nil,
subscriptionPeriod: parsedInvoice.subscriptionPeriod,
flags: [],
version: TelegramMediaInvoice.lastVersion
)
let botPaymentId = PeerId.init(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId))
return BotPaymentReceipt(invoice: parsedInvoice, date: date, info: nil, shippingOption: nil, credentialsTitle: "", invoiceMedia: invoiceMedia, tipAmount: nil, botPaymentId: botPaymentId, transactionId: transactionId)
}
}
|> castError(RequestBotPaymentReceiptError.self)
}
}
}
public struct BotPaymentInfo: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let paymentInfo = BotPaymentInfo(rawValue: 1 << 0)
public static let shippingInfo = BotPaymentInfo(rawValue: 1 << 1)
}
func _internal_clearBotPaymentInfo(network: Network, info: BotPaymentInfo) -> Signal<Void, NoError> {
var flags: Int32 = 0
if info.contains(.paymentInfo) {
flags |= (1 << 0)
}
if info.contains(.shippingInfo) {
flags |= (1 << 1)
}
return network.request(Api.functions.payments.clearSavedInfo(flags: flags))
|> retryRequest
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
@@ -0,0 +1,420 @@
import Foundation
import Postbox
import MtProtoKit
import SwiftSignalKit
import TelegramApi
public struct PremiumGiftCodeInfo: Equatable {
public let slug: String
public let fromPeerId: EnginePeer.Id?
public let messageId: EngineMessage.Id?
public let toPeerId: EnginePeer.Id?
public let date: Int32
public let months: Int32
public let usedDate: Int32?
public let isGiveaway: Bool
}
public struct PremiumGiftCodeOption: Codable, Equatable {
enum CodingKeys: String, CodingKey {
case users
case months
case storeProductId
case storeQuantity
case currency
case amount
}
public let users: Int32
public let months: Int32
public let storeProductId: String?
public let storeQuantity: Int32
public let currency: String
public let amount: Int64
public init(users: Int32, months: Int32, storeProductId: String?, storeQuantity: Int32, currency: String, amount: Int64) {
self.users = users
self.months = months
self.storeProductId = storeProductId
self.storeQuantity = storeQuantity
self.currency = currency
self.amount = amount
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.users = try container.decode(Int32.self, forKey: .users)
self.months = try container.decode(Int32.self, forKey: .months)
self.storeProductId = try container.decodeIfPresent(String.self, forKey: .storeProductId)
self.storeQuantity = try container.decodeIfPresent(Int32.self, forKey: .storeQuantity) ?? 1
self.currency = try container.decode(String.self, forKey: .currency)
self.amount = try container.decode(Int64.self, forKey: .amount)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.users, forKey: .users)
try container.encode(self.months, forKey: .months)
try container.encodeIfPresent(self.storeProductId, forKey: .storeProductId)
try container.encode(self.storeQuantity, forKey: .storeQuantity)
try container.encode(self.currency, forKey: .currency)
try container.encode(self.amount, forKey: .amount)
}
}
public enum PremiumGiveawayInfo: Equatable {
public enum OngoingStatus: Equatable {
public enum DisallowReason: Equatable {
case joinedTooEarly(Int32)
case channelAdmin(EnginePeer.Id)
case disallowedCountry(String)
}
case notQualified
case notAllowed(DisallowReason)
case participating
case almostOver
}
public enum ResultStatus: Equatable {
case notWon
case wonPremium(slug: String)
case wonStars(stars: Int64)
case refunded
}
case ongoing(startDate: Int32, status: OngoingStatus)
case finished(status: ResultStatus, startDate: Int32, finishDate: Int32, winnersCount: Int32, activatedCount: Int32?)
}
public struct PrepaidGiveaway: Equatable {
public enum Prize: Equatable {
case premium(months: Int32)
case stars(stars: Int64, boosts: Int32)
}
public let id: Int64
public let prize: Prize
public let quantity: Int32
public let date: Int32
}
func _internal_getPremiumGiveawayInfo(account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id) -> Signal<PremiumGiveawayInfo?, NoError> {
return account.postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer in
guard let inputPeer = apiInputPeer(peer) else {
return .complete()
}
return account.network.request(Api.functions.payments.getGiveawayInfo(peer: inputPeer, msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.GiveawayInfo?, NoError> in
return .single(nil)
}
|> map { result -> PremiumGiveawayInfo? in
if let result = result {
switch result {
case let .giveawayInfo(flags, startDate, joinedTooEarlyDate, adminDisallowedChatId, disallowedCountry):
if (flags & (1 << 3)) != 0 {
return .ongoing(startDate: startDate, status: .almostOver)
} else if (flags & (1 << 0)) != 0 {
return .ongoing(startDate: startDate, status: .participating)
} else if let disallowedCountry = disallowedCountry {
return .ongoing(startDate: startDate, status: .notAllowed(.disallowedCountry(disallowedCountry)))
} else if let joinedTooEarlyDate = joinedTooEarlyDate {
return .ongoing(startDate: startDate, status: .notAllowed(.joinedTooEarly(joinedTooEarlyDate)))
} else if let adminDisallowedChatId = adminDisallowedChatId {
return .ongoing(startDate: startDate, status: .notAllowed(.channelAdmin(EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(adminDisallowedChatId)))))
} else {
return .ongoing(startDate: startDate, status: .notQualified)
}
case let .giveawayInfoResults(flags, startDate, giftCodeSlug, stars, finishDate, winnersCount, activatedCount):
let status: PremiumGiveawayInfo.ResultStatus
if (flags & (1 << 1)) != 0 {
status = .refunded
} else if let stars {
status = .wonStars(stars: stars)
} else if let giftCodeSlug = giftCodeSlug {
status = .wonPremium(slug: giftCodeSlug)
} else {
status = .notWon
}
return .finished(status: status, startDate: startDate, finishDate: finishDate, winnersCount: winnersCount, activatedCount: activatedCount)
}
} else {
return nil
}
}
}
}
public final class CachedPremiumGiftCodeOptions: Codable {
public let options: [PremiumGiftCodeOption]
public init(options: [PremiumGiftCodeOption]) {
self.options = options
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.options = try container.decode([PremiumGiftCodeOption].self, forKey: "t")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.options, forKey: "t")
}
}
func _internal_premiumGiftCodeOptions(account: Account, peerId: EnginePeer.Id?, onlyCached: Bool = false) -> Signal<[PremiumGiftCodeOption], NoError> {
if let peerId {
if peerId.namespace == Namespaces.Peer.SecretChat {
return .single([])
}
}
let cached = account.postbox.transaction { transaction -> Signal<[PremiumGiftCodeOption], NoError> in
if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPremiumGiftCodeOptions, key: ValueBoxKey(length: 0)))?.get(CachedPremiumGiftCodeOptions.self) {
return .single(entry.options)
}
return .single([])
} |> switchToLatest
let remote = account.postbox.transaction { transaction -> Peer? in
if let peerId = peerId {
return transaction.getPeer(peerId)
}
return nil
}
|> mapToSignal { peer in
let inputPeer = peer.flatMap(apiInputPeer)
var flags: Int32 = 0
if let _ = inputPeer {
flags |= 1 << 0
}
return account.network.request(Api.functions.payments.getPremiumGiftCodeOptions(flags: flags, boostPeer: inputPeer))
|> map(Optional.init)
|> `catch` { _ -> Signal<[Api.PremiumGiftCodeOption]?, NoError> in
return .single(nil)
}
|> mapToSignal { results -> Signal<[PremiumGiftCodeOption], NoError> in
let options = results?.map { PremiumGiftCodeOption(apiGiftCodeOption: $0) } ?? []
return account.postbox.transaction { transaction -> [PremiumGiftCodeOption] in
if peerId == nil {
if let entry = CodableEntry(CachedPremiumGiftCodeOptions(options: options)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPremiumGiftCodeOptions, key: ValueBoxKey(length: 0)), entry: entry)
}
}
return options
}
}
}
if peerId == nil {
return cached
|> mapToSignal { cached in
if onlyCached && !cached.isEmpty {
return .single(cached)
} else {
return .single(cached)
|> then(remote)
}
}
} else {
return remote
}
}
func _internal_premiumGiftCodeOptions(account: Account, peerId: EnginePeer.Id?) -> Signal<[PremiumGiftCodeOption], NoError> {
if let peerId {
if peerId.namespace == Namespaces.Peer.SecretChat {
return .single([])
}
}
var flags: Int32 = 0
if let _ = peerId {
flags |= 1 << 0
}
return account.postbox.transaction { transaction -> Peer? in
if let peerId = peerId {
return transaction.getPeer(peerId)
}
return nil
}
|> mapToSignal { peer in
let inputPeer = peer.flatMap(apiInputPeer)
return account.network.request(Api.functions.payments.getPremiumGiftCodeOptions(flags: flags, boostPeer: inputPeer))
|> map(Optional.init)
|> `catch` { _ -> Signal<[Api.PremiumGiftCodeOption]?, NoError> in
return .single(nil)
}
|> mapToSignal { results -> Signal<[PremiumGiftCodeOption], NoError> in
if let results = results {
return .single(results.map { PremiumGiftCodeOption(apiGiftCodeOption: $0) })
} else {
return .single([])
}
}
}
}
func _internal_checkPremiumGiftCode(account: Account, slug: String) -> Signal<PremiumGiftCodeInfo?, NoError> {
return account.network.request(Api.functions.payments.checkGiftCode(slug: slug))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.CheckedGiftCode?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<PremiumGiftCodeInfo?, NoError> in
if let result = result {
switch result {
case let .checkedGiftCode(_, _, _, _, _, _, _, chats, users):
return account.postbox.transaction { transaction in
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers)
return PremiumGiftCodeInfo(apiCheckedGiftCode: result, slug: slug)
}
}
} else {
return .single(nil)
}
}
}
public enum ApplyPremiumGiftCodeError {
case generic
case waitForExpiration(Int32)
}
func _internal_applyPremiumGiftCode(account: Account, slug: String) -> Signal<Never, ApplyPremiumGiftCodeError> {
return account.network.request(Api.functions.payments.applyGiftCode(slug: slug))
|> mapError { error -> ApplyPremiumGiftCodeError in
if error.errorDescription.hasPrefix("PREMIUM_SUB_ACTIVE_UNTIL_") {
if let range = error.errorDescription.range(of: "_", options: .backwards) {
if let value = Int32(error.errorDescription[range.upperBound...]) {
return .waitForExpiration(value)
}
}
}
return .generic
}
|> mapToSignal { updates -> Signal<Never, ApplyPremiumGiftCodeError> in
account.stateManager.addUpdates(updates)
return .complete()
}
}
public enum LaunchPrepaidGiveawayError {
case generic
}
public enum LaunchGiveawayPurpose {
case premium
case stars(stars: Int64, users: Int32)
}
func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, purpose: LaunchGiveawayPurpose, id: Int64, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32) -> Signal<Never, LaunchPrepaidGiveawayError> {
return account.postbox.transaction { transaction -> Signal<Never, LaunchPrepaidGiveawayError> in
var flags: Int32 = 0
if onlyNewSubscribers {
flags |= (1 << 0)
}
if showWinners {
flags |= (1 << 3)
}
var inputPeer: Api.InputPeer?
if let peer = transaction.getPeer(peerId), let apiPeer = apiInputPeer(peer) {
inputPeer = apiPeer
}
var additionalPeers: [Api.InputPeer] = []
if !additionalPeerIds.isEmpty {
flags |= (1 << 1)
for peerId in additionalPeerIds {
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
additionalPeers.append(inputPeer)
}
}
}
if !countries.isEmpty {
flags |= (1 << 2)
}
if let _ = prizeDescription {
flags |= (1 << 4)
}
guard let inputPeer = inputPeer else {
return .complete()
}
let inputPurpose: Api.InputStorePaymentPurpose
switch purpose {
case let .stars(stars, users):
inputPurpose = .inputStorePaymentStarsGiveaway(flags: flags, stars: stars, boostPeer: inputPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: "", amount: 0, users: users)
case .premium:
inputPurpose = .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: inputPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: "", amount: 0)
}
return account.network.request(Api.functions.payments.launchPrepaidGiveaway(peer: inputPeer, giveawayId: id, purpose: inputPurpose))
|> mapError { _ -> LaunchPrepaidGiveawayError in
return .generic
}
|> mapToSignal { updates -> Signal<Never, LaunchPrepaidGiveawayError> in
account.stateManager.addUpdates(updates)
return .complete()
}
}
|> castError(LaunchPrepaidGiveawayError.self)
|> switchToLatest
}
extension PremiumGiftCodeOption {
init(apiGiftCodeOption: Api.PremiumGiftCodeOption) {
switch apiGiftCodeOption {
case let .premiumGiftCodeOption(_, users, months, storeProduct, storeQuantity, curreny, amount):
self.init(users: users, months: months, storeProductId: storeProduct, storeQuantity: storeQuantity ?? 1, currency: curreny, amount: amount)
}
}
}
extension PremiumGiftCodeInfo {
init(apiCheckedGiftCode: Api.payments.CheckedGiftCode, slug: String) {
switch apiCheckedGiftCode {
case let .checkedGiftCode(flags, fromId, giveawayMsgId, toId, date, months, usedDate, _, _):
self.slug = slug
self.fromPeerId = fromId?.peerId
if let fromId = fromId, let giveawayMsgId = giveawayMsgId {
self.messageId = EngineMessage.Id(peerId: fromId.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId)
} else {
self.messageId = nil
}
self.toPeerId = toId.flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value($0)) }
self.date = date
self.months = months
self.usedDate = usedDate
self.isGiveaway = (flags & (1 << 2)) != 0
}
}
}
public extension PremiumGiftCodeInfo {
var isUsed: Bool {
return self.usedDate != nil
}
}
extension PrepaidGiveaway {
init(apiPrepaidGiveaway: Api.PrepaidGiveaway) {
switch apiPrepaidGiveaway {
case let .prepaidGiveaway(id, months, quantity, date):
self.id = id
self.prize = .premium(months: months)
self.quantity = quantity
self.date = date
case let .prepaidStarsGiveaway(id, stars, quantity, boosts, date):
self.id = id
self.prize = .stars(stars: stars, boosts: boosts)
self.quantity = quantity
self.date = date
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,656 @@
import Foundation
import Postbox
import MtProtoKit
import SwiftSignalKit
import TelegramApi
public enum StarGiftAuctionReference: Equatable {
case giftId(Int64)
case slug(String)
var apiAuction: Api.InputStarGiftAuction {
switch self {
case let .giftId(giftId):
return .inputStarGiftAuction(giftId: giftId)
case let .slug(slug):
return .inputStarGiftAuctionSlug(slug: slug)
}
}
}
private func _internal_getStarGiftAuctionState(postbox: Postbox, network: Network, accountPeerId: EnginePeer.Id, reference: StarGiftAuctionReference, version: Int32) -> Signal<(gift: StarGift, state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)?, NoError> {
return network.request(Api.functions.payments.getStarGiftAuctionState(auction: reference.apiAuction, version: version))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.StarGiftAuctionState?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<(gift: StarGift, state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)?, NoError> in
guard let result else {
return .single(nil)
}
return postbox.transaction { transaction -> (gift: StarGift, state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)? in
switch result {
case let .starGiftAuctionState(apiGift, state, userState, timeout, users, chats):
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(chats: chats, users: users))
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return (
gift: gift,
state: GiftAuctionContext.State.AuctionState(apiAuctionState: state, transaction: transaction),
myState: GiftAuctionContext.State.MyState(apiAuctionUserState: userState),
timeout: timeout
)
}
}
}
}
public final class GiftAuctionContext {
public struct State: Equatable {
public struct BidLevel: Equatable {
public var position: Int32
public var amount: Int64
public var date: Int32
}
public enum Round: Equatable {
case generic(num: Int32, duration: Int32)
case extendable(num: Int32, duration: Int32, extendTop: Int32, extendWindow: Int32)
public var num: Int32 {
switch self {
case let .generic(num, _), let .extendable(num, _, _, _):
return num
}
}
public var duration: Int32 {
switch self {
case let .generic(_, duration), let .extendable(_, duration, _, _):
return duration
}
}
}
public enum AuctionState: Equatable {
case ongoing(version: Int32, startDate: Int32, endDate: Int32, minBidAmount: Int64, bidLevels: [BidLevel], topBidders: [EnginePeer], nextRoundDate: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32, rounds: [Round], lastGiftNumber: Int32)
case finished(startDate: Int32, endDate: Int32, averagePrice: Int64, listedCount: Int32?, fragmentListedCount: Int32?, fragmentListedUrl: String?)
}
public struct MyState: Equatable {
public var isReturned: Bool
public var bidAmount: Int64?
public var bidDate: Int32?
public var minBidAmount: Int64?
public var bidPeerId: EnginePeer.Id?
public var acquiredCount: Int32
}
public var gift: StarGift
public var auctionState: AuctionState
public var myState: MyState
}
private let queue: Queue = .mainQueue()
private let account: Account
public let gift: StarGift
public var isActive: Bool {
if case .finished = auctionState {
return false
} else {
return myState?.bidAmount != nil
}
}
private let disposable = MetaDisposable()
private var auctionState: State.AuctionState?
private var myState: State.MyState?
private var timeout: Int32?
private var updateTimer: SwiftSignalKit.Timer?
private let stateValue = Promise<State?>()
public var state: Signal<State?, NoError> {
return self.stateValue.get()
}
public var currentBidPeerId: EnginePeer.Id? {
if self.myState?.bidAmount != nil, case .ongoing = self.auctionState {
return self.myState?.bidPeerId
} else {
return nil
}
}
public var isFinished: Bool {
if case .finished = self.auctionState {
return true
} else {
return false
}
}
public var isUpcoming: Bool {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if case let .ongoing(_, startTime, _, _, _, _, _, _, _, _, _, _) = self.auctionState {
return currentTime < startTime
} else {
return false
}
}
public convenience init(account: Account, gift: StarGift) {
self.init(account: account, gift: gift, initialAuctionState: nil, initialMyState: nil, initialTimeout: nil)
}
init(account: Account, gift: StarGift, initialAuctionState: State.AuctionState?, initialMyState: State.MyState?, initialTimeout: Int32?) {
self.account = account
self.gift = gift
self.auctionState = initialAuctionState
self.myState = initialMyState
self.timeout = initialTimeout
self.load()
}
deinit {
self.updateTimer?.invalidate()
self.disposable.dispose()
}
private var currentVersion: Int32 {
var currentVersion: Int32 = 0
if case let .ongoing(version, _, _, _, _, _, _, _, _, _, _, _) = self.auctionState {
currentVersion = version
}
return currentVersion
}
public func load() {
self.pushState()
self.disposable.set((_internal_getStarGiftAuctionState(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, reference: .giftId(self.gift.giftId), version: self.currentVersion)
|> deliverOn(self.queue)).start(next: { [weak self] data in
guard let self else {
return
}
guard let (_, auctionState, myState, timeout) = data else {
return
}
if case let .ongoing(version, _, _, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion {
} else if let auctionState {
self.auctionState = auctionState
}
self.myState = myState
self.timeout = timeout
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var effectiveTimeout = timeout
if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, _, _, _, _) = auctionState {
let delta = nextRoundDate - currentTime
if delta > 0 && delta < timeout {
effectiveTimeout = delta
}
}
self.pushState()
self.updateTimer?.invalidate()
self.updateTimer = SwiftSignalKit.Timer(timeout: Double(effectiveTimeout), repeat: false, completion: { [weak self] _ in
guard let self else {
return
}
self.load()
}, queue: Queue.mainQueue())
self.updateTimer?.start()
}))
}
func updateAuctionState(_ auctionState: GiftAuctionContext.State.AuctionState) {
if case let .ongoing(version, _, _, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion {
} else {
self.auctionState = auctionState
}
self.pushState()
}
func updateMyState(_ myState: GiftAuctionContext.State.MyState) {
self.myState = myState
self.pushState()
}
private func pushState() {
if let auctionState = self.auctionState, let myState = self.myState {
self.stateValue.set(
.single(State(
gift: self.gift,
auctionState: auctionState,
myState: myState
))
)
} else {
self.stateValue.set(.single(nil))
}
}
}
extension GiftAuctionContext.State.BidLevel {
init(apiBidLevel: Api.AuctionBidLevel) {
switch apiBidLevel {
case let .auctionBidLevel(pos, amount, date):
self.position = pos
self.amount = amount
self.date = date
}
}
}
extension GiftAuctionContext.State.AuctionState {
init?(apiAuctionState: Api.StarGiftAuctionState, peers: [PeerId: Peer]) {
switch apiAuctionState {
case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBiddersPeerIds, nextRoundAt, lastGiftNumber, giftsLeft, currentRound, totalRounds, apiRounds):
var topBidders: [EnginePeer] = []
for peerId in topBiddersPeerIds {
if let peer = peers[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(peerId))] {
topBidders.append(EnginePeer(peer))
}
}
var rounds: [GiftAuctionContext.State.Round] = []
for apiRound in apiRounds {
switch apiRound {
case let .starGiftAuctionRound(num, duration):
rounds.append(.generic(num: num, duration: duration))
case let .starGiftAuctionRoundExtendable(num, duration, extendTop, extendWindow):
rounds.append(.extendable(num: num, duration: duration, extendTop: extendTop, extendWindow: extendWindow))
}
}
self = .ongoing(
version: version,
startDate: startDate,
endDate: endDate,
minBidAmount: minBidAmount,
bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)),
topBidders: topBidders,
nextRoundDate: nextRoundAt,
giftsLeft: giftsLeft,
currentRound: currentRound,
totalRounds: totalRounds,
rounds: rounds,
lastGiftNumber: lastGiftNumber
)
case let .starGiftAuctionStateFinished(_, startDate, endDate, averagePrice, listedCount, fragmentListedCount, fragmentListedUrl):
self = .finished(
startDate: startDate,
endDate: endDate,
averagePrice: averagePrice,
listedCount: listedCount,
fragmentListedCount: fragmentListedCount,
fragmentListedUrl: fragmentListedUrl
)
case .starGiftAuctionStateNotModified:
return nil
}
}
init?(apiAuctionState: Api.StarGiftAuctionState, transaction: Transaction) {
switch apiAuctionState {
case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBiddersPeerIds, nextRoundAt, lastGiftNumber, giftsLeft, currentRound, totalRounds, apiRounds):
var topBidders: [EnginePeer] = []
for peerId in topBiddersPeerIds {
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(peerId))) {
topBidders.append(EnginePeer(peer))
}
}
var rounds: [GiftAuctionContext.State.Round] = []
for apiRound in apiRounds {
switch apiRound {
case let .starGiftAuctionRound(num, duration):
rounds.append(.generic(num: num, duration: duration))
case let .starGiftAuctionRoundExtendable(num, duration, extendTop, extendWindow):
rounds.append(.extendable(num: num, duration: duration, extendTop: extendTop, extendWindow: extendWindow))
}
}
self = .ongoing(
version: version,
startDate: startDate,
endDate: endDate,
minBidAmount: minBidAmount,
bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)),
topBidders: topBidders,
nextRoundDate: nextRoundAt,
giftsLeft: giftsLeft,
currentRound: currentRound,
totalRounds: totalRounds,
rounds: rounds,
lastGiftNumber: lastGiftNumber
)
case let .starGiftAuctionStateFinished(_, startDate, endDate, averagePrice, listedCount, fragmentListedCount, fragmentListedUrl):
self = .finished(
startDate: startDate,
endDate: endDate,
averagePrice: averagePrice,
listedCount: listedCount,
fragmentListedCount: fragmentListedCount,
fragmentListedUrl: fragmentListedUrl
)
case .starGiftAuctionStateNotModified:
return nil
}
}
}
extension GiftAuctionContext.State.MyState {
init(apiAuctionUserState: Api.StarGiftAuctionUserState) {
switch apiAuctionUserState {
case let .starGiftAuctionUserState(flags, bidAmount, bidDate, minBidAmount, bidPeerId, acquiredCount):
self.isReturned = (flags & (1 << 1)) != 0
self.bidAmount = bidAmount
self.bidDate = bidDate
self.minBidAmount = minBidAmount
self.bidPeerId = bidPeerId?.peerId
self.acquiredCount = acquiredCount
}
}
}
public struct GiftAuctionAcquiredGift: Equatable {
public var nameHidden: Bool
public let peer: EnginePeer
public let date: Int32
public let bidAmount: Int64
public let round: Int32
public let position: Int32
public let text: String?
public let entities: [MessageTextEntity]?
public let number: Int32?
}
func _internal_getGiftAuctionAcquiredGifts(account: Account, giftId: Int64) -> Signal<[GiftAuctionAcquiredGift], NoError> {
return account.network.request(Api.functions.payments.getStarGiftAuctionAcquiredGifts(giftId: giftId))
|> map(Optional.init)
|> `catch` { _ in
return .single(nil)
}
|> mapToSignal { result in
guard let result else {
return .single([])
}
return account.postbox.transaction { transaction -> [GiftAuctionAcquiredGift] in
switch result {
case let .starGiftAuctionAcquiredGifts(gifts, users, chats):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers)
var mappedGifts: [GiftAuctionAcquiredGift] = []
for gift in gifts {
switch gift {
case let .starGiftAuctionAcquiredGift(flags, peerId, date, bidAmount, round, pos, message, number):
if let peer = transaction.getPeer(peerId.peerId) {
var text: String?
var entities: [MessageTextEntity]?
switch message {
case let .textWithEntities(textValue, entitiesValue):
text = textValue
entities = messageTextEntitiesFromApiEntities(entitiesValue)
default:
break
}
mappedGifts.append(GiftAuctionAcquiredGift(
nameHidden: (flags & (1 << 0)) != 0,
peer: EnginePeer(peer),
date: date,
bidAmount: bidAmount,
round: round,
position: pos,
text: text,
entities: entities,
number: number
))
}
}
}
return mappedGifts
}
}
}
}
func _internal_getActiveGiftAuctions(account: Account, hash: Int64) -> Signal<[GiftAuctionContext]?, NoError> {
return account.network.request(Api.functions.payments.getStarGiftActiveAuctions(hash: hash))
|> retryRequest
|> mapToSignal { result in
return account.postbox.transaction { transaction -> [GiftAuctionContext]? in
switch result {
case let .starGiftActiveAuctions(auctions, users, chats):
let parsedPeers = AccumulatedPeers(chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers)
var auctionContexts: [GiftAuctionContext] = []
for auction in auctions {
switch auction {
case let .starGiftActiveAuctionState(apiGift, auctionState, userState):
guard let gift = StarGift(apiStarGift: apiGift) else {
continue
}
auctionContexts.append(GiftAuctionContext(
account: account,
gift: gift,
initialAuctionState: GiftAuctionContext.State.AuctionState(apiAuctionState: auctionState, transaction: transaction),
initialMyState: GiftAuctionContext.State.MyState(apiAuctionUserState: userState),
initialTimeout: nil
))
}
}
return auctionContexts
case .starGiftActiveAuctionsNotModified:
return nil
}
}
}
}
public class GiftAuctionsManager {
private let account: Account
private var auctionContexts: [Int64 : GiftAuctionContext] = [:]
private let disposable = MetaDisposable()
private var updateAuctionStateDisposable: Disposable?
private var updateMyStateDisposable: Disposable?
private let statePromise = Promise<[GiftAuctionContext.State]>([])
public var state: Signal<[GiftAuctionContext.State], NoError> {
return self.statePromise.get()
}
public init(account: Account) {
self.account = account
self.updateAuctionStateDisposable = (self.account.stateManager.updatedStarGiftAuctionState()
|> deliverOnMainQueue).start(next: { [weak self] updates in
guard let self else {
return
}
var reload = false
for (giftId, update) in updates {
if let auctionContext = self.auctionContexts[giftId] {
auctionContext.updateAuctionState(update)
} else if case .ongoing = update {
reload = true
break
}
}
if reload {
self.reload()
}
})
self.updateMyStateDisposable = (self.account.stateManager.updatedStarGiftAuctionMyState()
|> deliverOnMainQueue).start(next: { [weak self] updates in
guard let self else {
return
}
var reload = false
for (giftId, update) in updates {
if let auctionContext = self.auctionContexts[giftId] {
auctionContext.updateMyState(update)
} else {
reload = true
break
}
}
if reload {
self.reload()
}
})
self.reload()
}
deinit {
self.disposable.dispose()
self.updateAuctionStateDisposable?.dispose()
self.updateMyStateDisposable?.dispose()
}
public func reload() {
self.disposable.set((_internal_getActiveGiftAuctions(account: self.account, hash: 0)
|> deliverOnMainQueue).startStrict(next: { [weak self] activeAuctions in
guard let self, let activeAuctions else {
return
}
for auction in activeAuctions {
if self.auctionContexts[auction.gift.giftId] == nil {
self.auctionContexts[auction.gift.giftId] = auction
}
}
self.updateState()
}))
}
public func auctionContext(for reference: StarGiftAuctionReference) -> Signal<GiftAuctionContext?, NoError> {
if case let .giftId(id) = reference, let current = self.auctionContexts[id] {
return .single(current)
} else {
return _internal_getStarGiftAuctionState(
postbox: self.account.postbox,
network: self.account.network,
accountPeerId: self.account.peerId,
reference: reference,
version: 0
) |> mapToSignal { [weak self] result in
if let self, let result {
let auctionContext = GiftAuctionContext(account: self.account, gift: result.gift, initialAuctionState: result.state, initialMyState: result.myState, initialTimeout: result.timeout)
self.auctionContexts[result.gift.giftId] = auctionContext
self.updateState()
return .single(auctionContext)
} else {
return .single(nil)
}
}
}
}
public func storeAuctionContext(auctionContext: GiftAuctionContext) {
self.auctionContexts[auctionContext.gift.giftId] = auctionContext
self.updateState()
}
private func updateState() {
var signals: [Signal<GiftAuctionContext.State?, NoError>] = []
for auction in self.auctionContexts.values.sorted(by: { $0.gift.giftId < $1.gift.giftId }) {
signals.append(auction.state)
}
self.statePromise.set(combineLatest(signals)
|> map { states -> [GiftAuctionContext.State] in
var filteredStates: [GiftAuctionContext.State] = []
for state in states {
if let state, case .ongoing = state.auctionState, state.myState.bidAmount != nil {
filteredStates.append(state)
}
}
return filteredStates
})
}
}
public extension GiftAuctionContext.State {
func getPlace(myBid: Int64?, myBidDate: Int32?) -> Int32? {
guard case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _, _, _) = self.auctionState else {
return nil
}
guard let myBid = myBid ?? self.myState.bidAmount else {
return nil
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let myBidDate = self.myState.bidDate ?? currentTime
let levels = bidLevels
guard !levels.isEmpty else {
return 1
}
func isWorse(than level: GiftAuctionContext.State.BidLevel) -> Bool {
if myBid < level.amount {
return true
}
if myBid == level.amount, myBidDate > level.date {
return true
}
return false
}
var lowerIndex: Int = -1
for (i, level) in levels.enumerated() {
if isWorse(than: level) {
lowerIndex = i
} else {
break
}
}
if lowerIndex == -1 {
return 1
}
let lowerPosition = levels[lowerIndex].position
let nextPosition: Int32
let nextIndex = lowerIndex + 1
if nextIndex < levels.count {
nextPosition = levels[nextIndex].position
} else {
nextPosition = lowerPosition
}
if nextPosition == lowerPosition + 1 {
return lowerPosition + 1
} else {
return nextPosition
}
}
var place: Int32? {
return self.getPlace(myBid: nil, myBidDate: nil)
}
var startDate: Int32 {
switch self.auctionState {
case let .ongoing(_, startDate, _, _, _, _, _, _, _, _, _, _):
return startDate
case let .finished(startDate, _, _, _, _, _):
return startDate
}
}
var endDate: Int32 {
switch self.auctionState {
case let .ongoing(_, _, endDate, _, _, _, _, _, _, _, _, _):
return endDate
case let .finished(_, endDate, _, _, _, _):
return endDate
}
}
}
@@ -0,0 +1,459 @@
import Foundation
import Postbox
import MtProtoKit
import SwiftSignalKit
import TelegramApi
public struct StarGiftCollection: Codable, Equatable {
public let id: Int32
public let title: String
public let icon: TelegramMediaFile?
public let count: Int32
public let hash: Int64
public init(id: Int32, title: String, icon: TelegramMediaFile?, count: Int32, hash: Int64) {
self.id = id
self.title = title
self.icon = icon
self.count = count
self.hash = hash
}
public static func ==(lhs: StarGiftCollection, rhs: StarGiftCollection) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.icon != rhs.icon {
return false
}
if lhs.count != rhs.count {
return false
}
if lhs.hash != rhs.hash {
return false
}
return true
}
}
extension StarGiftCollection {
init?(apiStarGiftCollection: Api.StarGiftCollection) {
switch apiStarGiftCollection {
case let .starGiftCollection(_, collectionId, title, icon, giftsCount, hash):
self.id = collectionId
self.title = title
self.icon = icon.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: nil) }
self.count = giftsCount
self.hash = hash
}
}
}
private final class CachedProfileGiftsCollections: Codable {
enum CodingKeys: String, CodingKey {
case collections
}
let collections: [StarGiftCollection]
init(collections: [StarGiftCollection]) {
self.collections = collections
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.collections = try container.decode([StarGiftCollection].self, forKey: .collections)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.collections, forKey: .collections)
}
}
private func entryId(peerId: EnginePeer.Id) -> ItemCacheEntryId {
let cacheKey = ValueBoxKey(length: 8)
cacheKey.setInt64(0, value: peerId.toInt64())
return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedProfileGiftsCollections, key: cacheKey)
}
private func intListSimpleHash(_ list: [Int64]) -> Int64 {
var acc: Int64 = 0
for value in list {
acc = ((acc * 20261) + Int64(0x80000000) + Int64(value)) % Int64(0x80000000)
}
return Int64(Int32(truncatingIfNeeded: acc))
}
private func _internal_getStarGiftCollections(postbox: Postbox, network: Network, peerId: EnginePeer.Id) -> Signal<[StarGiftCollection]?, NoError> {
return postbox.transaction { transaction -> (Api.InputPeer, [StarGiftCollection]?)? in
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) else {
return nil
}
let collections = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedProfileGiftsCollections.self)
return (inputPeer, collections?.collections)
}
|> mapToSignal { inputPeerAndCollections -> Signal<[StarGiftCollection]?, NoError> in
guard let (inputPeer, cachedCollections) = inputPeerAndCollections else {
return .single(nil)
}
var hash: Int64 = 0
if let cachedCollections {
hash = intListSimpleHash(cachedCollections.map { $0.hash })
}
return .single(cachedCollections)
|> then(
network.request(Api.functions.payments.getStarGiftCollections(peer: inputPeer, hash: hash))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.StarGiftCollections?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<[StarGiftCollection]?, NoError> in
guard let result else {
return .single(nil)
}
return postbox.transaction { transaction -> [StarGiftCollection]? in
switch result {
case let .starGiftCollections(collections):
let collections = collections.compactMap { StarGiftCollection(apiStarGiftCollection: $0) }
return collections
case .starGiftCollectionsNotModified:
return cachedCollections ?? []
}
}
}
)
}
}
private func _internal_createStarGiftCollection(account: Account, peerId: EnginePeer.Id, title: String, starGifts: [ProfileGiftsContext.State.StarGift]) -> Signal<StarGiftCollection?, NoError> {
return account.postbox.transaction { transaction -> (Api.InputPeer, [Api.InputSavedStarGift])? in
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) else {
return nil
}
let inputStarGifts = starGifts.compactMap { $0.reference }.compactMap { $0.apiStarGiftReference(transaction: transaction) }
return (inputPeer, inputStarGifts)
}
|> mapToSignal { inputPeerAndGifts -> Signal<StarGiftCollection?, NoError> in
guard let (inputPeer, inputStarGifts) = inputPeerAndGifts else {
return .single(nil)
}
return account.network.request(Api.functions.payments.createStarGiftCollection(peer: inputPeer, title: title, stargift: inputStarGifts))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.StarGiftCollection?, NoError> in
return .single(nil)
}
|> map { result -> StarGiftCollection? in
guard let result else {
return nil
}
return StarGiftCollection(apiStarGiftCollection: result)
}
|> beforeNext { collection in
let _ = account.postbox.transaction { transaction in
if let collection, let entry = CodableEntry(CachedProfileGifts(gifts: starGifts.map { $0.withPinnedToTop(false) }, count: Int32(starGifts.count), notificationsEnabled: nil)) {
transaction.putItemCacheEntry(id: giftsEntryId(peerId: peerId, collectionId: collection.id), entry: entry)
}
}.start()
}
}
}
private func _internal_reorderStarGiftCollections(account: Account, peerId: EnginePeer.Id, order: [Int32]) -> Signal<Bool, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Bool, NoError> in
guard let inputPeer else {
return .single(false)
}
return account.network.request(Api.functions.payments.reorderStarGiftCollections(peer: inputPeer, order: order))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Bool?, NoError> in
return .single(nil)
}
|> map { result -> Bool in
if let result, case .boolTrue = result {
return true
}
return false
}
}
}
private func _internal_updateStarGiftCollection(account: Account, peerId: EnginePeer.Id, collectionId: Int32, giftsContext: ProfileGiftsContext?, allGiftsContext: ProfileGiftsContext?, actions: [ProfileGiftsCollectionsContext.UpdateAction]) -> Signal<StarGiftCollection?, NoError> {
for action in actions {
switch action {
case let .addGifts(gifts):
let gifts = gifts.map { gift in
var collectionIds = gift.collectionIds ?? []
collectionIds.append(collectionId)
return gift.withCollectionIds(collectionIds)
}
giftsContext?.insertStarGifts(gifts: gifts)
case let .removeGifts(gifts):
giftsContext?.removeStarGifts(references: gifts)
case let .reorderGifts(gifts):
giftsContext?.reorderStarGifts(references: gifts)
default:
break
}
}
return account.postbox.transaction { transaction -> (Api.InputPeer, (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.StarGiftCollection>))? in
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) else {
return nil
}
var flags: Int32 = 0
var title: String?
var deleteStarGift: [Api.InputSavedStarGift] = []
var addStarGift: [Api.InputSavedStarGift] = []
var order: [Api.InputSavedStarGift] = []
for action in actions {
switch action {
case let .updateTitle(newTitle):
flags |= (1 << 0)
title = newTitle
case let .addGifts(gifts):
flags |= (1 << 2)
addStarGift.append(contentsOf: gifts.compactMap { $0.reference }.compactMap { $0.apiStarGiftReference(transaction: transaction) })
case let .removeGifts(gifts):
flags |= (1 << 1)
deleteStarGift.append(contentsOf: gifts.compactMap { $0.apiStarGiftReference(transaction: transaction) })
case let .reorderGifts(gifts):
flags |= (1 << 3)
order = gifts.compactMap { $0.apiStarGiftReference(transaction: transaction) }
}
}
let request = Api.functions.payments.updateStarGiftCollection(flags: flags, peer: inputPeer, collectionId: collectionId, title: title, deleteStargift: deleteStarGift, addStargift: addStarGift, order: order)
return (inputPeer, request)
}
|> mapToSignal { peerAndRequest -> Signal<StarGiftCollection?, NoError> in
guard let (_, request) = peerAndRequest else {
return .single(nil)
}
return account.network.request(request)
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.StarGiftCollection?, NoError> in
return .single(nil)
}
|> map { result -> StarGiftCollection? in
guard let result else {
return nil
}
return StarGiftCollection(apiStarGiftCollection: result)
}
}
}
private func _internal_deleteStarGiftCollection(account: Account, peerId: EnginePeer.Id, collectionId: Int32) -> Signal<Bool, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Bool, NoError> in
guard let inputPeer else {
return .single(false)
}
return account.network.request(Api.functions.payments.deleteStarGiftCollection(peer: inputPeer, collectionId: collectionId))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Bool?, NoError> in
return .single(nil)
}
|> map { result -> Bool in
if let result, case .boolTrue = result {
return true
}
return false
}
}
}
public final class ProfileGiftsCollectionsContext {
public struct State: Equatable {
public var collections: [StarGiftCollection]
public var isLoading: Bool
}
public enum UpdateAction {
case updateTitle(String)
case addGifts([ProfileGiftsContext.State.StarGift])
case removeGifts([StarGiftReference])
case reorderGifts([StarGiftReference])
}
private let queue: Queue = .mainQueue()
private let account: Account
private let peerId: EnginePeer.Id
private weak var allGiftsContext: ProfileGiftsContext?
private let disposable = MetaDisposable()
private var collections: [StarGiftCollection] = []
private var giftsContexts: [Int32: ProfileGiftsContext] = [:]
private var isLoading: Bool = false
private let stateValue = Promise<State>()
public var state: Signal<State, NoError> {
return self.stateValue.get()
}
public init(account: Account, peerId: EnginePeer.Id, allGiftsContext: ProfileGiftsContext?) {
self.account = account
self.peerId = peerId
self.allGiftsContext = allGiftsContext
self.reload()
}
deinit {
self.disposable.dispose()
}
public func giftsContextForCollection(id: Int32) -> ProfileGiftsContext {
if let current = self.giftsContexts[id] {
return current
} else {
let giftsContext = ProfileGiftsContext(account: self.account, peerId: self.peerId, collectionId: id)
self.giftsContexts[id] = giftsContext
return giftsContext
}
}
public func reload() {
guard !self.isLoading else { return }
self.isLoading = true
self.pushState()
self.disposable.set((_internal_getStarGiftCollections(postbox: self.account.postbox, network: self.account.network, peerId: self.peerId)
|> deliverOn(self.queue)).start(next: { [weak self] collections in
guard let self else {
return
}
self.collections = collections ?? []
self.isLoading = false
self.pushState()
self.updateCache()
}))
}
public func createCollection(title: String, starGifts: [ProfileGiftsContext.State.StarGift]) -> Signal<StarGiftCollection?, NoError> {
return _internal_createStarGiftCollection(account: self.account, peerId: self.peerId, title: title, starGifts: starGifts)
|> deliverOn(self.queue)
|> beforeNext { [weak self] collection in
guard let self else {
return
}
if let collection {
self.collections.append(collection)
self.pushState()
self.updateCache()
}
}
}
public func updateCollection(id: Int32, actions: [UpdateAction]) -> Signal<StarGiftCollection?, NoError> {
let giftsContext = self.giftsContextForCollection(id: id)
return _internal_updateStarGiftCollection(account: self.account, peerId: self.peerId, collectionId: id, giftsContext: giftsContext, allGiftsContext: self.allGiftsContext, actions: actions)
|> deliverOn(self.queue)
|> afterNext { [weak self] collection in
guard let self else {
return
}
if let collection {
if let index = self.collections.firstIndex(where: { $0.id == id }) {
self.collections[index] = collection
self.pushState()
self.updateCache()
}
}
}
}
public func addGifts(id: Int32, gifts: [ProfileGiftsContext.State.StarGift]) -> Signal<StarGiftCollection?, NoError> {
return self.updateCollection(id: id, actions: [.addGifts(gifts)])
}
public func removeGifts(id: Int32, gifts: [StarGiftReference]) -> Signal<StarGiftCollection?, NoError> {
return self.updateCollection(id: id, actions: [.removeGifts(gifts)])
}
public func reorderGifts(id: Int32, gifts: [StarGiftReference]) -> Signal<StarGiftCollection?, NoError> {
return self.updateCollection(id: id, actions: [.reorderGifts(gifts)])
}
public func renameCollection(id: Int32, title: String) -> Signal<StarGiftCollection?, NoError> {
return self.updateCollection(id: id, actions: [.updateTitle(title)])
}
public func reorderCollections(order: [Int32]) -> Signal<Bool, NoError> {
let peerId = self.peerId
return _internal_reorderStarGiftCollections(account: self.account, peerId: peerId, order: order)
|> deliverOn(self.queue)
|> afterNext { [weak self] collection in
guard let self else {
return
}
var collectionMap: [Int32: StarGiftCollection] = [:]
for collection in self.collections {
collectionMap[collection.id] = collection
}
var collections: [StarGiftCollection] = []
for id in order {
if let collection = collectionMap[id] {
collections.append(collection)
}
}
self.collections = collections
self.pushState()
self.updateCache()
}
}
public func deleteCollection(id: Int32) -> Signal<Bool, NoError> {
return _internal_deleteStarGiftCollection(account: self.account, peerId: self.peerId, collectionId: id)
|> deliverOn(self.queue)
|> afterNext { [weak self] _ in
guard let self else {
return
}
self.giftsContexts.removeValue(forKey: id)
self.collections.removeAll(where: { $0.id == id })
self.pushState()
self.updateCache()
}
}
private func updateCache() {
let peerId = self.peerId
let collections = self.collections
let _ = (self.account.postbox.transaction { transaction in
if let entry = CodableEntry(CachedProfileGiftsCollections(collections: collections)) {
transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry)
}
}).start()
}
private func pushState() {
let state = State(
collections: self.collections,
isLoading: self.isLoading
)
self.stateValue.set(.single(state))
}
}
@@ -0,0 +1,55 @@
import Foundation
import Postbox
import MtProtoKit
import SwiftSignalKit
import TelegramApi
public enum ResolveStarGiftOfferError {
case generic
}
func _internal_resolveStarGiftOffer(account: Account, messageId: EngineMessage.Id, accept: Bool) -> Signal<Never, ResolveStarGiftOfferError> {
var flags: Int32 = 0
if !accept {
flags |= (1 << 0)
}
return account.network.request(Api.functions.payments.resolveStarGiftOffer(flags: flags, offerMsgId: messageId.id))
|> mapError { _ -> ResolveStarGiftOfferError in
return .generic
}
|> mapToSignal { updates -> Signal<Never, ResolveStarGiftOfferError> in
account.stateManager.addUpdates(updates)
return .complete()
}
|> ignoreValues
}
public enum SendStarGiftOfferError {
case generic
}
func _internal_sendStarGiftOffer(account: Account, peerId: EnginePeer.Id, slug: String, amount: CurrencyAmount, duration: Int32, allowPaidStars: Int64?) -> Signal<Never, SendStarGiftOfferError> {
var flags: Int32 = 0
if let _ = allowPaidStars {
flags |= (1 << 0)
}
return account.postbox.transaction { transaction in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> castError(SendStarGiftOfferError.self)
|> mapToSignal { inputPeer -> Signal<Never, SendStarGiftOfferError> in
guard let inputPeer else {
return .fail(.generic)
}
return account.network.request(Api.functions.payments.sendStarGiftOffer(flags: flags, peer: inputPeer, slug: slug, price: amount.apiAmount, duration: duration, randomId: Int64.random(in: .min ..< .max), allowPaidStars: allowPaidStars))
|> mapError { _ -> SendStarGiftOfferError in
return .generic
}
|> mapToSignal { updates -> Signal<Never, SendStarGiftOfferError> in
account.stateManager.addUpdates(updates)
return .complete()
}
}
|> ignoreValues
}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More