mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Refactoring
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import SwiftSignalKit
|
||||
|
||||
public extension TelegramEngine {
|
||||
final class PeerManagement {
|
||||
private let account: Account
|
||||
|
||||
init(account: Account) {
|
||||
self.account = account
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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([])
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user