Refactoring

This commit is contained in:
Ali
2021-04-18 20:15:23 +04:00
parent 0ab87d6bbc
commit 2efced46b3
50 changed files with 301 additions and 298 deletions

View File

@@ -0,0 +1,165 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import SyncCore
public struct Country: PostboxCoding, 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: PostboxCoding, 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(decoder: PostboxDecoder) {
self.code = decoder.decodeStringForKey("c", orElse: "")
self.prefixes = decoder.decodeStringArrayForKey("pfx")
self.patterns = decoder.decodeStringArrayForKey("ptrn")
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeString(self.code, forKey: "c")
encoder.encodeStringArray(self.prefixes, forKey: "pfx")
encoder.encodeStringArray(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(decoder: PostboxDecoder) {
self.id = decoder.decodeStringForKey("c", orElse: "")
self.name = decoder.decodeStringForKey("n", orElse: "")
self.localizedName = decoder.decodeOptionalStringForKey("ln")
self.countryCodes = decoder.decodeObjectArrayForKey("cc").map { $0 as! CountryCode }
self.hidden = decoder.decodeBoolForKey("h", orElse: false)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeString(self.id, forKey: "c")
encoder.encodeString(self.name, forKey: "n")
if let localizedName = self.localizedName {
encoder.encodeString(localizedName, forKey: "ln")
} else {
encoder.encodeNil(forKey: "ln")
}
encoder.encodeObjectArray(self.countryCodes, forKey: "cc")
encoder.encodeBool(self.hidden, forKey: "h")
}
}
public final class CountriesList: PreferencesEntry, Equatable {
public let countries: [Country]
public let hash: Int32
public init(countries: [Country], hash: Int32) {
self.countries = countries
self.hash = hash
}
public init(decoder: PostboxDecoder) {
self.countries = decoder.decodeObjectArrayForKey("c").map { $0 as! Country }
self.hash = decoder.decodeInt32ForKey("h", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObjectArray(self.countries, forKey: "c")
encoder.encodeInt32(self.hash, forKey: "h")
}
public func isEqual(to: PreferencesEntry) -> Bool {
if let to = to as? CountriesList {
return self == to
} else {
return false
}
}
public static func ==(lhs: CountriesList, rhs: CountriesList) -> Bool {
return lhs.countries == rhs.countries && lhs.hash == rhs.hash
}
}
func _internal_getCountriesList(accountManager: AccountManager, 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 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] as? CountriesList {
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)
}
}
}

View File

@@ -0,0 +1,30 @@
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, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> {
return _internal_getCountriesList(accountManager: accountManager, network: self.account.network, langCode: langCode, forceUpdate: forceUpdate)
}
}
}
public extension TelegramEngineUnauthorized {
final class Localization {
private let account: UnauthorizedAccount
init(account: UnauthorizedAccount) {
self.account = account
}
public func getCountriesList(accountManager: AccountManager, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> {
return _internal_getCountriesList(accountManager: accountManager, network: self.account.network, langCode: langCode, forceUpdate: forceUpdate)
}
}
}

View File

@@ -0,0 +1,13 @@
import SwiftSignalKit
public extension TelegramEngine {
final class PeerManagement {
private let account: Account
init(account: Account) {
self.account = account
}
}
}

View File

@@ -0,0 +1,299 @@
import Foundation
import Postbox
import MtProtoKit
import SwiftSignalKit
import TelegramApi
import SyncCore
public enum RequestSecureIdFormError {
case generic
case serverError(String)
case versionOutdated
}
private func parseSecureValueType(_ type: Api.SecureValueType, selfie: Bool, translation: Bool, nativeNames: Bool) -> SecureIdRequestedFormFieldValue {
switch type {
case .secureValueTypePersonalDetails:
return .personalDetails(nativeName: nativeNames)
case .secureValueTypePassport:
return .passport(selfie: selfie, translation: translation)
case .secureValueTypeInternalPassport:
return .internalPassport(selfie: selfie, translation: translation)
case .secureValueTypeDriverLicense:
return .driversLicense(selfie: selfie, translation: translation)
case .secureValueTypeIdentityCard:
return .idCard(selfie: selfie, translation: translation)
case .secureValueTypeAddress:
return .address
case .secureValueTypeUtilityBill:
return .utilityBill(translation: translation)
case .secureValueTypeBankStatement:
return .bankStatement(translation: translation)
case .secureValueTypeRentalAgreement:
return .rentalAgreement(translation: translation)
case .secureValueTypePhone:
return .phone
case .secureValueTypeEmail:
return .email
case .secureValueTypePassportRegistration:
return .passportRegistration(translation: translation)
case .secureValueTypeTemporaryRegistration:
return .temporaryRegistration(translation: translation)
}
}
private func parseSecureData(_ value: Api.SecureData) -> (data: Data, hash: Data, secret: Data) {
switch value {
case let .secureData(data, dataHash, secret):
return (data.makeData(), dataHash.makeData(), secret.makeData())
}
}
struct ParsedSecureValue {
let valueWithContext: SecureIdValueWithContext
}
func parseSecureValue(context: SecureIdAccessContext, value: Api.SecureValue, errors: [Api.SecureValueError]) -> ParsedSecureValue? {
switch value {
case let .secureValue(_, type, data, frontSide, reverseSide, selfie, translation, files, plainData, hash):
let parsedFileReferences = files.flatMap { $0.compactMap(SecureIdFileReference.init) } ?? []
let parsedFiles = parsedFileReferences.map(SecureIdVerificationDocumentReference.remote)
let parsedTranslationReferences = translation.flatMap { $0.compactMap(SecureIdFileReference.init) } ?? []
let parsedTranslations = parsedTranslationReferences.map(SecureIdVerificationDocumentReference.remote)
let parsedFrontSide = frontSide.flatMap(SecureIdFileReference.init).flatMap(SecureIdVerificationDocumentReference.remote)
let parsedBackSide = reverseSide.flatMap(SecureIdFileReference.init).flatMap(SecureIdVerificationDocumentReference.remote)
let parsedSelfie = selfie.flatMap(SecureIdFileReference.init).flatMap(SecureIdVerificationDocumentReference.remote)
let decryptedData: Data?
let encryptedMetadata: SecureIdEncryptedValueMetadata?
var parsedFileMetadata: [SecureIdEncryptedValueFileMetadata] = []
var parsedTranslationMetadata: [SecureIdEncryptedValueFileMetadata] = []
var parsedSelfieMetadata: SecureIdEncryptedValueFileMetadata?
var parsedFrontSideMetadata: SecureIdEncryptedValueFileMetadata?
var parsedBackSideMetadata: SecureIdEncryptedValueFileMetadata?
var contentsId: Data?
if let data = data {
let (encryptedData, decryptedHash, encryptedSecret) = parseSecureData(data)
guard let valueContext = decryptedSecureValueAccessContext(context: context, encryptedSecret: encryptedSecret, decryptedDataHash: decryptedHash) else {
return nil
}
contentsId = decryptedHash
decryptedData = decryptedSecureValueData(context: valueContext, encryptedData: encryptedData, decryptedDataHash: decryptedHash)
if decryptedData == nil {
return nil
}
encryptedMetadata = SecureIdEncryptedValueMetadata(valueDataHash: decryptedHash, decryptedSecret: valueContext.secret)
} else {
decryptedData = nil
encryptedMetadata = nil
}
for file in parsedFileReferences {
guard let fileSecret = decryptedSecureIdFileSecret(context: context, fileHash: file.fileHash, encryptedSecret: file.encryptedSecret) else {
return nil
}
parsedFileMetadata.append(SecureIdEncryptedValueFileMetadata(hash: file.fileHash, secret: fileSecret))
}
for file in parsedTranslationReferences {
guard let fileSecret = decryptedSecureIdFileSecret(context: context, fileHash: file.fileHash, encryptedSecret: file.encryptedSecret) else {
return nil
}
parsedTranslationMetadata.append(SecureIdEncryptedValueFileMetadata(hash: file.fileHash, secret: fileSecret))
}
if let parsedSelfie = selfie.flatMap(SecureIdFileReference.init) {
guard let fileSecret = decryptedSecureIdFileSecret(context: context, fileHash: parsedSelfie.fileHash, encryptedSecret: parsedSelfie.encryptedSecret) else {
return nil
}
parsedSelfieMetadata = SecureIdEncryptedValueFileMetadata(hash: parsedSelfie.fileHash, secret: fileSecret)
}
if let parsedFrontSide = frontSide.flatMap(SecureIdFileReference.init) {
guard let fileSecret = decryptedSecureIdFileSecret(context: context, fileHash: parsedFrontSide.fileHash, encryptedSecret: parsedFrontSide.encryptedSecret) else {
return nil
}
parsedFrontSideMetadata = SecureIdEncryptedValueFileMetadata(hash: parsedFrontSide.fileHash, secret: fileSecret)
}
if let parsedBackSide = reverseSide.flatMap(SecureIdFileReference.init) {
guard let fileSecret = decryptedSecureIdFileSecret(context: context, fileHash: parsedBackSide.fileHash, encryptedSecret: parsedBackSide.encryptedSecret) else {
return nil
}
parsedBackSideMetadata = SecureIdEncryptedValueFileMetadata(hash: parsedBackSide.fileHash, secret: fileSecret)
}
let value: SecureIdValue
switch type {
case .secureValueTypePersonalDetails:
guard let dict = (try? JSONSerialization.jsonObject(with: decryptedData ?? Data(), options: [])) as? [String: Any] else {
return nil
}
guard let personalDetails = SecureIdPersonalDetailsValue(dict: dict, fileReferences: parsedFiles) else {
return nil
}
value = .personalDetails(personalDetails)
case .secureValueTypePassport:
guard let dict = (try? JSONSerialization.jsonObject(with: decryptedData ?? Data(), options: [])) as? [String: Any] else {
return nil
}
guard let passport = SecureIdPassportValue(dict: dict, fileReferences: parsedFiles, translations: parsedTranslations, selfieDocument: parsedSelfie, frontSideDocument: parsedFrontSide) else {
return nil
}
value = .passport(passport)
case .secureValueTypeInternalPassport:
guard let dict = (try? JSONSerialization.jsonObject(with: decryptedData ?? Data(), options: [])) as? [String: Any] else {
return nil
}
guard let internalPassport = SecureIdInternalPassportValue(dict: dict, fileReferences: parsedFiles, translations: parsedTranslations, selfieDocument: parsedSelfie, frontSideDocument: parsedFrontSide) else {
return nil
}
value = .internalPassport(internalPassport)
case .secureValueTypeDriverLicense:
guard let dict = (try? JSONSerialization.jsonObject(with: decryptedData ?? Data(), options: [])) as? [String: Any] else {
return nil
}
guard let driversLicense = SecureIdDriversLicenseValue(dict: dict, fileReferences: parsedFiles, translations: parsedTranslations, selfieDocument: parsedSelfie, frontSideDocument: parsedFrontSide, backSideDocument: parsedBackSide) else {
return nil
}
value = .driversLicense(driversLicense)
case .secureValueTypeIdentityCard:
guard let dict = (try? JSONSerialization.jsonObject(with: decryptedData ?? Data(), options: [])) as? [String: Any] else {
return nil
}
guard let idCard = SecureIdIDCardValue(dict: dict, fileReferences: parsedFiles, translations: parsedTranslations, selfieDocument: parsedSelfie, frontSideDocument: parsedFrontSide, backSideDocument: parsedBackSide) else {
return nil
}
value = .idCard(idCard)
case .secureValueTypeAddress:
guard let dict = (try? JSONSerialization.jsonObject(with: decryptedData ?? Data(), options: [])) as? [String: Any] else {
return nil
}
guard let address = SecureIdAddressValue(dict: dict, fileReferences: parsedFiles) else {
return nil
}
value = .address(address)
case .secureValueTypePassportRegistration:
guard let passportRegistration = SecureIdPassportRegistrationValue(fileReferences: parsedFiles, translations: parsedTranslations) else {
return nil
}
value = .passportRegistration(passportRegistration)
case .secureValueTypeTemporaryRegistration:
guard let temporaryRegistration = SecureIdTemporaryRegistrationValue(fileReferences: parsedFiles, translations: parsedTranslations) else {
return nil
}
value = .temporaryRegistration(temporaryRegistration)
case .secureValueTypeUtilityBill:
guard let utilityBill = SecureIdUtilityBillValue(fileReferences: parsedFiles, translations: parsedTranslations) else {
return nil
}
value = .utilityBill(utilityBill)
case .secureValueTypeBankStatement:
guard let bankStatement = SecureIdBankStatementValue(fileReferences: parsedFiles, translations: parsedTranslations) else {
return nil
}
value = .bankStatement(bankStatement)
case .secureValueTypeRentalAgreement:
guard let rentalAgreement = SecureIdRentalAgreementValue(fileReferences: parsedFiles, translations: parsedTranslations) else {
return nil
}
value = .rentalAgreement(rentalAgreement)
case .secureValueTypePhone:
guard let publicData = plainData else {
return nil
}
switch publicData {
case let .securePlainPhone(phone):
value = .phone(SecureIdPhoneValue(phone: phone))
default:
return nil
}
case .secureValueTypeEmail:
guard let publicData = plainData else {
return nil
}
switch publicData {
case let .securePlainEmail(email):
value = .email(SecureIdEmailValue(email: email))
default:
return nil
}
}
return ParsedSecureValue(valueWithContext: SecureIdValueWithContext(value: value, errors: parseSecureIdValueContentErrors(dataHash: contentsId, fileHashes: Set(parsedFileMetadata.map { $0.hash } + parsedTranslationMetadata.map { $0.hash}), selfieHash: parsedSelfieMetadata?.hash, frontSideHash: parsedFrontSideMetadata?.hash, backSideHash: parsedBackSideMetadata?.hash, errors: errors), files: parsedFileMetadata, translations: parsedTranslationMetadata, selfie: parsedSelfieMetadata, frontSide: parsedFrontSideMetadata, backSide: parsedBackSideMetadata, encryptedMetadata: encryptedMetadata, opaqueHash: hash.makeData()))
}
}
private func parseSecureValues(context: SecureIdAccessContext, values: [Api.SecureValue], errors: [Api.SecureValueError], requestedFields: [SecureIdRequestedFormField]) -> [SecureIdValueWithContext] {
return values.map({ apiValue in
return parseSecureValue(context: context, value: apiValue, errors: errors)
}).compactMap({ $0?.valueWithContext })
}
public struct EncryptedSecureIdForm {
public let peerId: PeerId
public let requestedFields: [SecureIdRequestedFormField]
public let termsUrl: String?
let encryptedValues: [Api.SecureValue]
let errors: [Api.SecureValueError]
}
public func requestSecureIdForm(postbox: Postbox, network: Network, peerId: PeerId, scope: String, publicKey: String) -> Signal<EncryptedSecureIdForm, RequestSecureIdFormError> {
if peerId.namespace != Namespaces.Peer.CloudUser {
return .fail(.serverError("BOT_INVALID"))
}
if scope.isEmpty {
return .fail(.serverError("SCOPE_EMPTY"))
}
if publicKey.isEmpty {
return .fail(.serverError("PUBLIC_KEY_REQUIRED"))
}
return network.request(Api.functions.account.getAuthorizationForm(botId: peerId.id._internalGetInt32Value(), scope: scope, publicKey: publicKey))
|> mapError { error -> RequestSecureIdFormError in
switch error.errorDescription {
case "APP_VERSION_OUTDATED":
return .versionOutdated
default:
return .serverError(error.errorDescription)
}
}
|> mapToSignal { result -> Signal<EncryptedSecureIdForm, RequestSecureIdFormError> in
return postbox.transaction { transaction -> EncryptedSecureIdForm in
switch result {
case let .authorizationForm(_, requiredTypes, values, errors, users, termsUrl):
var peers: [Peer] = []
for user in users {
let parsed = TelegramUser(user: user)
peers.append(parsed)
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated in
return updated
})
return EncryptedSecureIdForm(peerId: peerId, requestedFields: requiredTypes.map { requiredType in
switch requiredType {
case let .secureRequiredType(flags, type):
return .just(parseSecureValueType(type, selfie: (flags & 1 << 1) != 0, translation: (flags & 1 << 2) != 0, nativeNames: (flags & 1 << 0) != 0))
case let .secureRequiredTypeOneOf(types):
let parsedInnerTypes = types.compactMap { innerType -> SecureIdRequestedFormFieldValue? in
switch innerType {
case let .secureRequiredType(flags, type):
return parseSecureValueType(type, selfie: (flags & 1 << 1) != 0, translation: (flags & 1 << 2) != 0, nativeNames: (flags & 1 << 0) != 0)
case .secureRequiredTypeOneOf:
return nil
}
}
return .oneOf(parsedInnerTypes)
}
}, termsUrl: termsUrl, encryptedValues: values, errors: errors)
}
} |> mapError { _ in return RequestSecureIdFormError.generic }
}
}
public func decryptedSecureIdForm(context: SecureIdAccessContext, form: EncryptedSecureIdForm) -> SecureIdForm? {
return SecureIdForm(peerId: form.peerId, requestedFields: form.requestedFields, values: parseSecureValues(context: context, values: form.encryptedValues, errors: form.errors, requestedFields: form.requestedFields))
}

View File

@@ -0,0 +1,61 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
import SyncCore
public enum ArchivedStickerPacksNamespace: Int32 {
case stickers = 0
case masks = 1
var itemCollectionNamespace: ItemCollectionId.Namespace {
switch self {
case .stickers:
return Namespaces.ItemCollection.CloudStickerPacks
case .masks:
return Namespaces.ItemCollection.CloudMaskPacks
}
}
}
public final class ArchivedStickerPackItem {
public let info: StickerPackCollectionInfo
public let topItems: [StickerPackItem]
public init(info: StickerPackCollectionInfo, topItems: [StickerPackItem]) {
self.info = info
self.topItems = topItems
}
}
func _internal_archivedStickerPacks(account: Account, namespace: ArchivedStickerPacksNamespace = .stickers) -> Signal<[ArchivedStickerPackItem], NoError> {
var flags: Int32 = 0
if case .masks = namespace {
flags |= 1 << 0
}
return account.network.request(Api.functions.messages.getArchivedStickers(flags: flags, offsetId: 0, limit: 200))
|> map { result -> [ArchivedStickerPackItem] in
var archivedItems: [ArchivedStickerPackItem] = []
switch result {
case let .archivedStickers(_, sets):
for set in sets {
let (info, items) = parsePreviewStickerSet(set, namespace: namespace.itemCollectionNamespace)
archivedItems.append(ArchivedStickerPackItem(info: info, topItems: items))
}
}
return archivedItems
} |> `catch` { _ in
return .single([])
}
}
func _internal_removeArchivedStickerPack(account: Account, info: StickerPackCollectionInfo) -> Signal<Void, NoError> {
return account.network.request(Api.functions.messages.uninstallStickerSet(stickerset: Api.InputStickerSet.inputStickerSetID(id: info.id.id, accessHash: info.accessHash)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}

View File

@@ -0,0 +1,194 @@
import Foundation
import Postbox
import SwiftSignalKit
import MurMurHash32
import SyncCore
private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 100, highWaterItemCount: 200)
public enum CachedStickerPackResult {
case none
case fetching
case result(StickerPackCollectionInfo, [ItemCollectionItem], Bool)
}
func cacheStickerPack(transaction: Transaction, info: StickerPackCollectionInfo, items: [ItemCollectionItem], reference: StickerPackReference? = nil) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(info.id)), entry: CachedStickerPack(info: info, items: items.map { $0 as! StickerPackItem }, hash: info.hash), collectionSpec: collectionSpec)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: info.shortName.lowercased())), entry: CachedStickerPack(info: info, items: items.map { $0 as! StickerPackItem }, hash: info.hash), collectionSpec: collectionSpec)
if let reference = reference {
var namespace: Int32?
var id: ItemCollectionId.Id?
switch reference {
case .animatedEmoji:
namespace = Namespaces.ItemCollection.CloudAnimatedEmoji
id = 0
case let .dice(emoji):
namespace = Namespaces.ItemCollection.CloudDice
id = Int64(murMurHashString32(emoji))
default:
break
}
if let namespace = namespace, let id = id {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id))), entry: CachedStickerPack(info: info, items: items.map { $0 as! StickerPackItem }, hash: info.hash), collectionSpec: collectionSpec)
}
}
}
func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceRemote: Bool) -> Signal<CachedStickerPackResult, NoError> {
return postbox.transaction { transaction -> CachedStickerPackResult? in
if let (info, items, local) = cachedStickerPack(transaction: transaction, reference: reference) {
if local {
return .result(info, items, true)
}
}
return nil
}
|> mapToSignal { value -> Signal<CachedStickerPackResult, NoError> in
if let value = value {
return .single(value)
} else {
return postbox.transaction { transaction -> (CachedStickerPackResult, Bool, Int32?) in
let namespace = Namespaces.ItemCollection.CloudStickerPacks
var previousHash: Int32?
switch reference {
case let .id(id, _):
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id)))) as? CachedStickerPack, let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case let .name(shortName):
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName.lowercased()))) as? CachedStickerPack, let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case .animatedEmoji:
let namespace = Namespaces.ItemCollection.CloudAnimatedEmoji
let id: ItemCollectionId.Id = 0
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id)))) as? CachedStickerPack, let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
case let .dice(emoji):
let namespace = Namespaces.ItemCollection.CloudDice
let id: ItemCollectionId.Id = Int64(murMurHashString32(emoji))
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id)))) as? CachedStickerPack, let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
}
}
|> mapToSignal { result, loadRemote, previousHash in
if loadRemote || forceRemote {
let appliedRemote = updatedRemoteStickerPack(postbox: postbox, network: network, reference: reference)
|> mapToSignal { result -> Signal<CachedStickerPackResult, NoError> in
if let result = result, result.0.hash == previousHash {
return .complete()
}
return postbox.transaction { transaction -> CachedStickerPackResult in
if let result = result {
cacheStickerPack(transaction: transaction, info: result.0, items: result.1, reference: reference)
let currentInfo = transaction.getItemCollectionInfo(collectionId: result.0.id) as? StickerPackCollectionInfo
return .result(result.0, result.1, currentInfo != nil)
} else {
return .none
}
}
}
return .single(result)
|> then(appliedRemote)
} else {
return .single(result)
}
}
}
}
}
func cachedStickerPack(transaction: Transaction, reference: StickerPackReference) -> (StickerPackCollectionInfo, [ItemCollectionItem], Bool)? {
let namespaces: [Int32] = [Namespaces.ItemCollection.CloudStickerPacks, Namespaces.ItemCollection.CloudMaskPacks]
switch reference {
case let .id(id, _):
for namespace in namespaces {
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (currentInfo, items, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id)))) as? CachedStickerPack, let info = cached.info {
return (info, cached.items, false)
}
}
case let .name(shortName):
for namespace in namespaces {
for info in transaction.getItemCollectionsInfos(namespace: namespace) {
if let info = info.1 as? StickerPackCollectionInfo {
if info.shortName == shortName {
let items = transaction.getItemCollectionItems(collectionId: info.id)
if !items.isEmpty {
return (info, items, true)
}
}
}
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName.lowercased()))) as? CachedStickerPack, let info = cached.info {
return (info, cached.items, false)
}
case .animatedEmoji:
let namespace = Namespaces.ItemCollection.CloudAnimatedEmoji
let id: ItemCollectionId.Id = 0
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (currentInfo, items, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id)))) as? CachedStickerPack, let info = cached.info {
return (info, cached.items, false)
}
case let .dice(emoji):
let namespace = Namespaces.ItemCollection.CloudDice
let id: ItemCollectionId.Id = Int64(murMurHashString32(emoji))
if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo {
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id))
if !items.isEmpty {
return (currentInfo, items, true)
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id)))) as? CachedStickerPack, let info = cached.info {
return (info, cached.items, false)
}
}
return nil
}

View File

@@ -0,0 +1,92 @@
import Foundation
import Postbox
import SwiftSignalKit
import SyncCore
private let refreshTimeout: Int32 = 60 * 60
private enum SearchEmojiKeywordsIntermediateResult {
case updating(timestamp: Int32?)
case completed([EmojiKeywordItem])
}
func _internal_searchEmojiKeywords(postbox: Postbox, inputLanguageCode: String, query: String, completeMatch: Bool) -> Signal<[EmojiKeywordItem], NoError> {
guard !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return .single([])
}
let collectionId = emojiKeywordColletionIdForCode(inputLanguageCode)
let search: (Transaction) -> [EmojiKeywordItem] = { transaction in
let queryTokens = stringIndexTokens(query, transliteration: .none)
if let firstQueryToken = queryTokens.first {
let query: ItemCollectionSearchQuery = completeMatch ? .exact(firstQueryToken) : .matching(queryTokens)
let items = transaction.searchItemCollection(namespace: Namespaces.ItemCollection.EmojiKeywords, query: query).filter { item -> Bool in
if let item = item as? EmojiKeywordItem, item.collectionId == collectionId.id {
return true
} else {
return false
}
} as? [EmojiKeywordItem]
if let items = items {
return items.sorted(by: { lhs, rhs -> Bool in
if lhs.keyword.count == rhs.keyword.count {
return lhs.keyword < rhs.keyword
} else {
return lhs.keyword.count < rhs.keyword.count
}
})
}
}
return []
}
return postbox.transaction { transaction -> Signal<SearchEmojiKeywordsIntermediateResult, NoError> in
let currentTime = Int32(CFAbsoluteTimeGetCurrent())
let info = transaction.getItemCollectionInfo(collectionId: collectionId)
if let info = info as? EmojiKeywordCollectionInfo {
if info.timestamp + refreshTimeout < currentTime {
addSynchronizeEmojiKeywordsOperation(transaction: transaction, inputLanguageCode: inputLanguageCode, languageCode: info.languageCode, fromVersion: info.version)
return .single(.updating(timestamp: info.timestamp))
} else {
return .single(.completed(search(transaction)))
}
} else {
addSynchronizeEmojiKeywordsOperation(transaction: transaction, inputLanguageCode: inputLanguageCode, languageCode: nil, fromVersion: nil)
return .single(.updating(timestamp: nil))
}
}
|> switchToLatest
|> mapToSignal { intermediateResult -> Signal<[EmojiKeywordItem], NoError> in
switch intermediateResult {
case let .updating(timestamp):
return postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.EmojiKeywords], aroundIndex: nil, count: 10)
|> filter { view -> Bool in
for info in view.collectionInfos {
if let info = info.1 as? EmojiKeywordCollectionInfo, info.id == collectionId {
if let timestamp = timestamp {
return timestamp < info.timestamp
} else {
return true
}
}
}
return false
}
|> take(1)
|> mapToSignal { view -> Signal<[EmojiKeywordItem], NoError> in
for info in view.collectionInfos {
if let info = info.1 as? EmojiKeywordCollectionInfo, info.id == collectionId {
return postbox.transaction { transaction -> [EmojiKeywordItem] in
return search(transaction)
}
}
}
return .complete()
}
case let .completed(items):
return .single(items)
}
}
}

View File

@@ -0,0 +1,110 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
import SyncCore
extension StickerPackReference {
init(_ stickerPackInfo: StickerPackCollectionInfo) {
self = .id(id: stickerPackInfo.id.id, accessHash: stickerPackInfo.accessHash)
}
var apiInputStickerSet: Api.InputStickerSet {
switch self {
case let .id(id, accessHash):
return .inputStickerSetID(id: id, accessHash: accessHash)
case let .name(name):
return .inputStickerSetShortName(shortName: name)
case .animatedEmoji:
return .inputStickerSetAnimatedEmoji
case let .dice(emoji):
return .inputStickerSetDice(emoticon: emoji)
}
}
}
public enum LoadedStickerPack {
case fetching
case none
case result(info: StickerPackCollectionInfo, items: [ItemCollectionItem], installed: Bool)
}
func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference) -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem])?, NoError> {
return network.request(Api.functions.messages.getStickerSet(stickerset: reference.apiInputStickerSet))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.StickerSet?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem])?, NoError> in
guard let result = result else {
return .single(nil)
}
let info: StickerPackCollectionInfo
var items: [ItemCollectionItem] = []
switch result {
case let .stickerSet(set, packs, documents):
let namespace: ItemCollectionId.Namespace
switch set {
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 3)) != 0 {
namespace = Namespaces.ItemCollection.CloudMaskPacks
} else {
namespace = Namespaces.ItemCollection.CloudStickerPacks
}
}
info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs {
switch pack {
case let .stickerPack(text, fileIds):
let key = ValueBoxKey(text).toMemoryBuffer()
for fileId in fileIds {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for apiDocument in documents {
if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id {
let fileIndexKeys: [MemoryBuffer]
if let indexKeys = indexKeysByFile[id] {
fileIndexKeys = indexKeys
} else {
fileIndexKeys = []
}
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: fileIndexKeys))
}
}
}
return postbox.transaction { transaction -> (StickerPackCollectionInfo, [ItemCollectionItem])? in
if transaction.getItemCollectionInfo(collectionId: info.id) != nil {
transaction.replaceItemCollectionItems(collectionId: info.id, items: items)
}
cacheStickerPack(transaction: transaction, info: info, items: items)
return (info, items)
}
}
}
func _internal_loadedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceActualized: Bool) -> Signal<LoadedStickerPack, NoError> {
return _internal_cachedStickerPack(postbox: postbox, network: network, reference: reference, forceRemote: forceActualized)
|> map { result -> LoadedStickerPack in
switch result {
case .none:
return .none
case .fetching:
return .fetching
case let .result(info, items, installed):
return .result(info: info, items: items, installed: installed)
}
}
}

View File

@@ -0,0 +1,358 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
import SyncCore
private struct SearchStickersConfiguration {
static var defaultValue: SearchStickersConfiguration {
return SearchStickersConfiguration(cacheTimeout: 86400)
}
public let cacheTimeout: Int32
fileprivate init(cacheTimeout: Int32) {
self.cacheTimeout = cacheTimeout
}
static func with(appConfiguration: AppConfiguration) -> SearchStickersConfiguration {
if let data = appConfiguration.data, let value = data["stickers_emoji_cache_time"] as? Int32 {
return SearchStickersConfiguration(cacheTimeout: value)
} else {
return .defaultValue
}
}
}
public final class FoundStickerItem: Equatable {
public let file: TelegramMediaFile
public let stringRepresentations: [String]
public init(file: TelegramMediaFile, stringRepresentations: [String]) {
self.file = file
self.stringRepresentations = stringRepresentations
}
public static func ==(lhs: FoundStickerItem, rhs: FoundStickerItem) -> Bool {
if !lhs.file.isEqual(to: rhs.file) {
return false
}
if lhs.stringRepresentations != rhs.stringRepresentations {
return false
}
return true
}
}
private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 100, highWaterItemCount: 200)
public struct SearchStickersScope: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let installed = SearchStickersScope(rawValue: 1 << 0)
public static let remote = SearchStickersScope(rawValue: 1 << 1)
}
func _internal_randomGreetingSticker(account: Account) -> Signal<FoundStickerItem?, NoError> {
return account.postbox.transaction { transaction -> FoundStickerItem? in
var stickerItems: [FoundStickerItem] = []
for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudGreetingStickers) {
if let item = entry.contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile {
stickerItems.append(FoundStickerItem(file: file, stringRepresentations: []))
}
}
return stickerItems.randomElement()
}
}
func _internal_searchStickers(account: Account, query: String, scope: SearchStickersScope = [.installed, .remote]) -> Signal<[FoundStickerItem], NoError> {
if scope.isEmpty {
return .single([])
}
var query = query
if query == "\u{2764}" {
query = "\u{2764}\u{FE0F}"
}
return account.postbox.transaction { transaction -> ([FoundStickerItem], CachedStickerQueryResult?) in
var result: [FoundStickerItem] = []
if scope.contains(.installed) {
for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers) {
if let item = entry.contents as? SavedStickerItem {
for representation in item.stringRepresentations {
if representation.hasPrefix(query) {
result.append(FoundStickerItem(file: item.file, stringRepresentations: item.stringRepresentations))
break
}
}
}
}
let currentItems = Set<MediaId>(result.map { $0.file.fileId })
var recentItems: [TelegramMediaFile] = []
var recentAnimatedItems: [TelegramMediaFile] = []
var recentItemsIds = Set<MediaId>()
var matchingRecentItemsIds = Set<MediaId>()
for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentStickers) {
if let item = entry.contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile {
if !currentItems.contains(file.fileId) {
for case let .Sticker(displayText, _, _) in file.attributes {
if displayText.hasPrefix(query) {
matchingRecentItemsIds.insert(file.fileId)
}
recentItemsIds.insert(file.fileId)
if file.isAnimatedSticker {
recentAnimatedItems.append(file)
} else {
recentItems.append(file)
}
break
}
}
}
}
var searchQuery: ItemCollectionSearchQuery = .exact(ValueBoxKey(query))
if query == "\u{2764}" {
searchQuery = .any([ValueBoxKey("\u{2764}"), ValueBoxKey("\u{2764}\u{FE0F}")])
}
var installedItems: [FoundStickerItem] = []
var installedAnimatedItems: [FoundStickerItem] = []
for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudStickerPacks, query: searchQuery) {
if let item = item as? StickerPackItem {
if !currentItems.contains(item.file.fileId) {
var stringRepresentations: [String] = []
for key in item.indexKeys {
key.withDataNoCopy { data in
if let string = String(data: data, encoding: .utf8) {
stringRepresentations.append(string)
}
}
}
if !recentItemsIds.contains(item.file.fileId) {
if item.file.isAnimatedSticker {
installedAnimatedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations))
} else {
installedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations))
}
} else {
matchingRecentItemsIds.insert(item.file.fileId)
}
}
}
}
for file in recentAnimatedItems {
if matchingRecentItemsIds.contains(file.fileId) {
result.append(FoundStickerItem(file: file, stringRepresentations: [query]))
}
}
for file in recentItems {
if matchingRecentItemsIds.contains(file.fileId) {
result.append(FoundStickerItem(file: file, stringRepresentations: [query]))
}
}
result.append(contentsOf: installedAnimatedItems)
result.append(contentsOf: installedItems)
}
var cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query))) as? CachedStickerQueryResult
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration ?? AppConfiguration.defaultValue
let searchStickersConfiguration = SearchStickersConfiguration.with(appConfiguration: appConfiguration)
if let currentCached = cached, currentTime > currentCached.timestamp + searchStickersConfiguration.cacheTimeout {
cached = nil
}
return (result, cached)
} |> mapToSignal { localItems, cached -> Signal<[FoundStickerItem], NoError> in
var tempResult: [FoundStickerItem] = localItems
if !scope.contains(.remote) {
return .single(tempResult)
}
let currentItemIds = Set<MediaId>(localItems.map { $0.file.fileId })
if let cached = cached {
var cachedItems: [FoundStickerItem] = []
var cachedAnimatedItems: [FoundStickerItem] = []
for file in cached.items {
if !currentItemIds.contains(file.fileId) {
if file.isAnimatedSticker {
cachedAnimatedItems.append(FoundStickerItem(file: file, stringRepresentations: []))
} else {
cachedItems.append(FoundStickerItem(file: file, stringRepresentations: []))
}
}
}
tempResult.append(contentsOf: cachedAnimatedItems)
tempResult.append(contentsOf: cachedItems)
}
let remote = account.network.request(Api.functions.messages.getStickers(emoticon: query, hash: cached?.hash ?? 0))
|> `catch` { _ -> Signal<Api.messages.Stickers, NoError> in
return .single(.stickersNotModified)
}
|> mapToSignal { result -> Signal<[FoundStickerItem], NoError> in
return account.postbox.transaction { transaction -> [FoundStickerItem] in
switch result {
case let .stickers(hash, stickers):
var items: [FoundStickerItem] = []
var animatedItems: [FoundStickerItem] = []
var result: [FoundStickerItem] = localItems
let currentItemIds = Set<MediaId>(result.map { $0.file.fileId })
var files: [TelegramMediaFile] = []
for sticker in stickers {
if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id {
files.append(file)
if !currentItemIds.contains(id) {
if file.isAnimatedSticker {
animatedItems.append(FoundStickerItem(file: file, stringRepresentations: []))
} else {
items.append(FoundStickerItem(file: file, stringRepresentations: []))
}
}
}
}
result.append(contentsOf: animatedItems)
result.append(contentsOf: items)
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query)), entry: CachedStickerQueryResult(items: files, hash: hash, timestamp: currentTime), collectionSpec: collectionSpec)
return result
case .stickersNotModified:
break
}
return tempResult
}
}
return .single(tempResult)
|> then(remote)
}
}
public struct FoundStickerSets {
public var infos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)]
public let entries: [ItemCollectionViewEntry]
public init(infos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)] = [], entries: [ItemCollectionViewEntry] = []) {
self.infos = infos
self.entries = entries
}
public func withUpdatedInfosAndEntries(infos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)], entries: [ItemCollectionViewEntry]) -> FoundStickerSets {
let infoResult = self.infos + infos
let entriesResult = self.entries + entries
return FoundStickerSets(infos: infoResult, entries: entriesResult)
}
public func merge(with other: FoundStickerSets) -> FoundStickerSets {
return FoundStickerSets(infos: self.infos + other.infos, entries: self.entries + other.entries)
}
}
func _internal_searchStickerSetsRemotely(network: Network, query: String) -> Signal<FoundStickerSets, NoError> {
return network.request(Api.functions.messages.searchStickerSets(flags: 0, q: query, hash: 0))
|> mapError {_ in}
|> mapToSignal { value in
var index: Int32 = 1000
switch value {
case let .foundStickerSets(_, sets: sets):
var result = FoundStickerSets()
for set in sets {
let parsed = parsePreviewStickerSet(set)
let values = parsed.1.map({ ItemCollectionViewEntry(index: ItemCollectionViewEntryIndex(collectionIndex: index, collectionId: parsed.0.id, itemIndex: $0.index), item: $0) })
result = result.withUpdatedInfosAndEntries(infos: [(parsed.0.id, parsed.0, parsed.1.first, false)], entries: values)
index += 1
}
return .single(result)
default:
break
}
return .complete()
}
|> `catch` { _ -> Signal<FoundStickerSets, NoError> in
return .single(FoundStickerSets())
}
}
func _internal_searchStickerSets(postbox: Postbox, query: String) -> Signal<FoundStickerSets, NoError> {
return postbox.transaction { transaction -> Signal<FoundStickerSets, NoError> in
let infos = transaction.getItemCollectionsInfos(namespace: Namespaces.ItemCollection.CloudStickerPacks)
var collections: [(ItemCollectionId, ItemCollectionInfo)] = []
var topItems: [ItemCollectionId: ItemCollectionItem] = [:]
var entries: [ItemCollectionViewEntry] = []
for info in infos {
if let info = info.1 as? StickerPackCollectionInfo {
let split = info.title.split(separator: " ")
if !split.filter({$0.lowercased().hasPrefix(query.lowercased())}).isEmpty || info.shortName.lowercased().hasPrefix(query.lowercased()) {
collections.append((info.id, info))
}
}
}
var index: Int32 = 0
for info in collections {
let items = transaction.getItemCollectionItems(collectionId: info.0)
let values = items.map({ ItemCollectionViewEntry(index: ItemCollectionViewEntryIndex(collectionIndex: index, collectionId: info.0, itemIndex: $0.index), item: $0) })
entries.append(contentsOf: values)
if let first = items.first {
topItems[info.0] = first
}
index += 1
}
let result = FoundStickerSets(infos: collections.map { ($0.0, $0.1, topItems[$0.0], true) }, entries: entries)
return .single(result)
} |> switchToLatest
}
func _internal_searchGifs(account: Account, query: String, nextOffset: String = "") -> Signal<ChatContextResultCollection?, NoError> {
return account.postbox.transaction { transaction -> String in
let configuration = currentSearchBotsConfiguration(transaction: transaction)
return configuration.gifBotUsername ?? "gif"
} |> mapToSignal {
return _internal_resolvePeerByName(account: account, name: $0)
} |> filter { $0 != nil }
|> map { $0! }
|> mapToSignal { peerId -> Signal<Peer, NoError> in
return account.postbox.loadedPeerWithId(peerId)
}
|> mapToSignal { peer -> Signal<ChatContextResultCollection?, NoError> in
return requestChatContextResults(account: account, botId: peer.id, peerId: account.peerId, query: query, offset: nextOffset)
|> map { results -> ChatContextResultCollection? in
return results?.results
}
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
return .single(nil)
}
}
}
extension TelegramMediaFile {
var stickerString: String? {
for attr in attributes {
if case let .Sticker(displayText, _, _) = attr {
return displayText
}
}
return nil
}
}

View File

@@ -0,0 +1,110 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
import SyncCore
import MtProtoKit
func telegramStickerPackThumbnailRepresentationFromApiSizes(datacenterId: Int32, thumbVersion: Int32?, sizes: [Api.PhotoSize]) -> (immediateThumbnail: Data?, representations: [TelegramMediaImageRepresentation]) {
var immediateThumbnailData: Data?
var representations: [TelegramMediaImageRepresentation] = []
for size in sizes {
switch size {
case let .photoCachedSize(_, w, h, _):
let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil))
case let .photoSize(_, w, h, _):
let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil))
case let .photoSizeProgressive(_, w, h, sizes):
let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil))
case let .photoPathSize(_, data):
immediateThumbnailData = data.makeData()
case .photoStrippedSize:
break
case .photoSizeEmpty:
break
}
}
return (immediateThumbnailData, representations)
}
extension StickerPackCollectionInfo {
convenience init(apiSet: Api.StickerSet, namespace: ItemCollectionId.Namespace) {
switch apiSet {
case let .stickerSet(flags, _, id, accessHash, title, shortName, thumbs, thumbDcId, thumbVersion, count, nHash):
var setFlags: StickerPackCollectionInfoFlags = StickerPackCollectionInfoFlags()
if (flags & (1 << 2)) != 0 {
setFlags.insert(.isOfficial)
}
if (flags & (1 << 3)) != 0 {
setFlags.insert(.isMasks)
}
if (flags & (1 << 5)) != 0 {
setFlags.insert(.isAnimated)
}
var thumbnailRepresentation: TelegramMediaImageRepresentation?
var immediateThumbnailData: Data?
if let thumbs = thumbs, let thumbDcId = thumbDcId {
let (data, representations) = telegramStickerPackThumbnailRepresentationFromApiSizes(datacenterId: thumbDcId, thumbVersion: thumbVersion, sizes: thumbs)
thumbnailRepresentation = representations.first
immediateThumbnailData = data
}
self.init(id: ItemCollectionId(namespace: namespace, id: id), flags: setFlags, accessHash: accessHash, title: title, shortName: shortName, thumbnail: thumbnailRepresentation, immediateThumbnailData: immediateThumbnailData, hash: nHash, count: count)
}
}
}
func _internal_stickerPacksAttachedToMedia(account: Account, media: AnyMediaReference) -> Signal<[StickerPackReference], NoError> {
let inputMedia: Api.InputStickeredMedia
let resourceReference: MediaResourceReference
if let imageReference = media.concrete(TelegramMediaImage.self), let reference = imageReference.media.reference, case let .cloud(imageId, accessHash, fileReference) = reference, let representation = largestImageRepresentation(imageReference.media.representations) {
inputMedia = .inputStickeredMediaPhoto(id: Api.InputPhoto.inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: fileReference ?? Data())))
resourceReference = imageReference.resourceReference(representation.resource)
} else if let fileReference = media.concrete(TelegramMediaFile.self), let resource = fileReference.media.resource as? CloudDocumentMediaResource {
inputMedia = .inputStickeredMediaDocument(id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())))
resourceReference = fileReference.resourceReference(fileReference.media.resource)
} else {
return .single([])
}
return account.network.request(Api.functions.messages.getAttachedStickers(media: inputMedia))
|> `catch` { _ -> Signal<[Api.StickerSetCovered], MTRpcError> in
return revalidateMediaResourceReference(postbox: account.postbox, network: account.network, revalidationContext: account.mediaReferenceRevalidationContext, info: TelegramCloudMediaResourceFetchInfo(reference: resourceReference, preferBackgroundReferenceRevalidation: false, continueInBackground: false), resource: resourceReference.resource)
|> mapError { _ -> MTRpcError in
return MTRpcError(errorCode: 500, errorDescription: "Internal")
}
|> mapToSignal { reference -> Signal<[Api.StickerSetCovered], MTRpcError> in
let inputMedia: Api.InputStickeredMedia
if let resource = reference.updatedResource as? TelegramCloudMediaResourceWithFileReference, let updatedReference = resource.fileReference {
if let imageReference = media.concrete(TelegramMediaImage.self), let reference = imageReference.media.reference, case let .cloud(imageId, accessHash, _) = reference, let _ = largestImageRepresentation(imageReference.media.representations) {
inputMedia = .inputStickeredMediaPhoto(id: Api.InputPhoto.inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: updatedReference)))
} else if let fileReference = media.concrete(TelegramMediaFile.self), let resource = fileReference.media.resource as? CloudDocumentMediaResource {
inputMedia = .inputStickeredMediaDocument(id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: updatedReference)))
} else {
return .single([])
}
return account.network.request(Api.functions.messages.getAttachedStickers(media: inputMedia))
} else {
return .single([])
}
}
|> `catch` { _ -> Signal<[Api.StickerSetCovered], MTRpcError> in
return .single([])
}
}
|> map { result -> [StickerPackReference] in
return result.map { pack in
switch pack {
case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _):
let info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks)
return .id(id: info.id.id, accessHash: info.accessHash)
}
}
}
|> `catch` { _ -> Signal<[StickerPackReference], NoError> in
return .single([])
}
}

View File

@@ -0,0 +1,111 @@
import Foundation
import Postbox
import SwiftSignalKit
import SyncCore
func _internal_addStickerPackInteractively(postbox: Postbox, info: StickerPackCollectionInfo, items: [ItemCollectionItem], positionInList: Int? = nil) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
let namespace: SynchronizeInstalledStickerPacksOperationNamespace?
switch info.id.namespace {
case Namespaces.ItemCollection.CloudStickerPacks:
namespace = .stickers
case Namespaces.ItemCollection.CloudMaskPacks:
namespace = .masks
default:
namespace = nil
}
if let namespace = namespace {
var mappedInfo = info
if items.isEmpty {
mappedInfo = StickerPackCollectionInfo(id: info.id, flags: info.flags, accessHash: info.accessHash, title: info.title, shortName: info.shortName, thumbnail: info.thumbnail, immediateThumbnailData: info.immediateThumbnailData, hash: Int32(bitPattern: arc4random()), count: info.count)
}
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: .add([mappedInfo.id]), noDelay: items.isEmpty)
var updatedInfos = transaction.getItemCollectionsInfos(namespace: mappedInfo.id.namespace).map { $0.1 as! StickerPackCollectionInfo }
if let index = updatedInfos.firstIndex(where: { $0.id == mappedInfo.id }) {
let currentInfo = updatedInfos[index]
updatedInfos.remove(at: index)
updatedInfos.insert(currentInfo, at: 0)
} else {
if let positionInList = positionInList, positionInList <= updatedInfos.count {
updatedInfos.insert(mappedInfo, at: positionInList)
} else {
updatedInfos.insert(mappedInfo, at: 0)
}
transaction.replaceItemCollectionItems(collectionId: mappedInfo.id, items: items)
}
transaction.replaceItemCollectionInfos(namespace: mappedInfo.id.namespace, itemCollectionInfos: updatedInfos.map { ($0.id, $0) })
}
}
}
public enum RemoveStickerPackOption {
case delete
case archive
}
func _internal_removeStickerPackInteractively(postbox: Postbox, id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> {
return _internal_removeStickerPacksInteractively(postbox: postbox, ids: [id], option: option)
}
func _internal_removeStickerPacksInteractively(postbox: Postbox, ids: [ItemCollectionId], option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> {
return postbox.transaction { transaction -> (Int, [ItemCollectionItem])? in
var commonNamespace: SynchronizeInstalledStickerPacksOperationNamespace?
for id in ids {
let namespace: SynchronizeInstalledStickerPacksOperationNamespace?
switch id.namespace {
case Namespaces.ItemCollection.CloudStickerPacks:
namespace = .stickers
case Namespaces.ItemCollection.CloudMaskPacks:
namespace = .masks
default:
namespace = nil
}
if commonNamespace == nil && namespace != nil {
commonNamespace = namespace
} else if commonNamespace != namespace {
fatalError()
}
}
if let namespace = commonNamespace {
let content: AddSynchronizeInstalledStickerPacksOperationContent
switch option {
case .delete:
content = .remove(ids)
case .archive:
content = .archive(ids)
}
if let id = ids.first {
let index = transaction.getItemCollectionsInfos(namespace: id.namespace).firstIndex(where: { $0.0 == id })
let items = transaction.getItemCollectionItems(collectionId: id)
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: content, noDelay: false)
transaction.removeItemCollection(collectionId: id)
return index.flatMap { ($0, items) }
} else {
return nil
}
} else {
return nil
}
}
}
func _internal_markFeaturedStickerPacksAsSeenInteractively(postbox: Postbox, ids: [ItemCollectionId]) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
let idsSet = Set(ids)
var items = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
var readIds = Set<ItemCollectionId>()
for i in 0 ..< items.count {
let item = (items[i].contents as! FeaturedStickerPackItem)
if item.unread && idsSet.contains(item.info.id) {
readIds.insert(item.info.id)
items[i] = OrderedItemListEntry(id: items[i].id, contents: FeaturedStickerPackItem(info: item.info, topItems: item.topItems, unread: false))
}
}
if !readIds.isEmpty {
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks, items: items)
addSynchronizeMarkFeaturedStickerPacksAsSeenOperation(transaction: transaction, ids: Array(readIds))
}
}
}

View File

@@ -0,0 +1,73 @@
import SwiftSignalKit
import SyncCore
import Postbox
public extension TelegramEngine {
final class Stickers {
private let account: Account
init(account: Account) {
self.account = account
}
public func archivedStickerPacks(namespace: ArchivedStickerPacksNamespace = .stickers) -> Signal<[ArchivedStickerPackItem], NoError> {
return _internal_archivedStickerPacks(account: account, namespace: namespace)
}
public func removeArchivedStickerPack(info: StickerPackCollectionInfo) -> Signal<Void, NoError> {
return _internal_removeArchivedStickerPack(account: self.account, info: info)
}
public func cachedStickerPack(reference: StickerPackReference, forceRemote: Bool) -> Signal<CachedStickerPackResult, NoError> {
return _internal_cachedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceRemote: forceRemote)
}
public func loadedStickerPack(reference: StickerPackReference, forceActualized: Bool) -> Signal<LoadedStickerPack, NoError> {
return _internal_loadedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceActualized: forceActualized)
}
public func randomGreetingSticker() -> Signal<FoundStickerItem?, NoError> {
return _internal_randomGreetingSticker(account: self.account)
}
public func searchStickers(query: String, scope: SearchStickersScope = [.installed, .remote]) -> Signal<[FoundStickerItem], NoError> {
return _internal_searchStickers(account: self.account, query: query, scope: scope)
}
public func searchStickerSetsRemotely(query: String) -> Signal<FoundStickerSets, NoError> {
return _internal_searchStickerSetsRemotely(network: self.account.network, query: query)
}
public func searchStickerSets(query: String) -> Signal<FoundStickerSets, NoError> {
return _internal_searchStickerSets(postbox: self.account.postbox, query: query)
}
public func searchGifs(query: String, nextOffset: String = "") -> Signal<ChatContextResultCollection?, NoError> {
return _internal_searchGifs(account: self.account, query: query, nextOffset: nextOffset)
}
public func addStickerPackInteractively(info: StickerPackCollectionInfo, items: [ItemCollectionItem], positionInList: Int? = nil) -> Signal<Void, NoError> {
return _internal_addStickerPackInteractively(postbox: self.account.postbox, info: info, items: items, positionInList: positionInList)
}
public func removeStickerPackInteractively(id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> {
return _internal_removeStickerPackInteractively(postbox: self.account.postbox, id: id, option: option)
}
public func removeStickerPacksInteractively(ids: [ItemCollectionId], option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> {
return _internal_removeStickerPacksInteractively(postbox: self.account.postbox, ids: ids, option: option)
}
public func markFeaturedStickerPacksAsSeenInteractively(ids: [ItemCollectionId]) -> Signal<Void, NoError> {
return _internal_markFeaturedStickerPacksAsSeenInteractively(postbox: self.account.postbox, ids: ids)
}
public func searchEmojiKeywords(inputLanguageCode: String, query: String, completeMatch: Bool) -> Signal<[EmojiKeywordItem], NoError> {
return _internal_searchEmojiKeywords(postbox: self.account.postbox, inputLanguageCode: inputLanguageCode, query: query, completeMatch: completeMatch)
}
public func stickerPacksAttachedToMedia(media: AnyMediaReference) -> Signal<[StickerPackReference], NoError> {
return _internal_stickerPacksAttachedToMedia(account: self.account, media: media)
}
}
}

View File

@@ -31,6 +31,18 @@ public final class TelegramEngine {
public lazy var accountData: AccountData = {
return AccountData(account: self.account)
}()
public lazy var stickers: Stickers = {
return Stickers(account: self.account)
}()
public lazy var peerManagement: PeerManagement = {
return PeerManagement(account: self.account)
}()
public lazy var localization: Localization = {
return Localization(account: self.account)
}()
}
public final class TelegramEngineUnauthorized {
@@ -43,4 +55,8 @@ public final class TelegramEngineUnauthorized {
public lazy var auth: Auth = {
return Auth(account: self.account)
}()
public lazy var localization: Localization = {
return Localization(account: self.account)
}()
}