import Foundation import TelegramApi import Postbox import SwiftSignalKit import MtProtoKit enum FeaturedStickerPacksCategory { case stickerPacks case emojiPacks } extension FeaturedStickerPacksCategory { var itemListNamespace: Int32 { switch self { case .stickerPacks: return Namespaces.OrderedItemList.CloudFeaturedStickerPacks case .emojiPacks: return Namespaces.OrderedItemList.CloudFeaturedEmojiPacks } } var collectionIdNamespace: Int32 { switch self { case .stickerPacks: return Namespaces.ItemCollection.CloudStickerPacks case .emojiPacks: return Namespaces.ItemCollection.CloudEmojiPacks } } } private func hashForIdsReverse(_ ids: [Int64]) -> Int64 { var acc: UInt64 = 0 for id in ids { combineInt64Hash(&acc, with: UInt64(bitPattern: id)) } return Int64(bitPattern: acc) } private func hashForIdsReverse(_ ids: [Int64], unreadIds: [Int64]) -> Int64 { var acc: UInt64 = 0 for id in ids { combineInt64Hash(&acc, with: UInt64(bitPattern: id)) if unreadIds.contains(id) { combineInt64Hash(&acc, with: 1 as UInt64) } } return Int64(bitPattern: acc) } func manageStickerPacks(network: Network, postbox: Postbox) -> Signal { return (postbox.transaction { transaction -> Void in addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .stickers, content: .sync, noDelay: false) addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .masks, content: .sync, noDelay: false) addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .emoji, content: .sync, noDelay: false) addSynchronizeSavedGifsOperation(transaction: transaction, operation: .sync) addSynchronizeSavedStickersOperation(transaction: transaction, operation: .sync) addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .sync) } |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } func resolveMissingStickerSets(network: Network, postbox: Postbox, stickerSets: [Api.StickerSetCovered], ignorePacksWithHashes: [Int64: Int32]) -> Signal<[Api.StickerSetCovered], NoError> { var missingSignals: [Signal<(Int, Api.StickerSetCovered)?, NoError>] = [] for i in 0 ..< stickerSets.count { switch stickerSets[i] { case let .stickerSetNoCovered(value), let .stickerSetCovered(value, _): switch value { case let .stickerSet(_, _, id, accessHash, _, _, _, _, _, _, _, hash): if ignorePacksWithHashes[id] == hash { continue } missingSignals.append(network.request(Api.functions.messages.getStickerSet(stickerset: .inputStickerSetID(id: id, accessHash: accessHash), hash: 0)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> map { result -> (Int, Api.StickerSetCovered)? in if let result = result { switch result { case let .stickerSet(set, packs, keywords, documents): return (i, Api.StickerSetCovered.stickerSetFullCovered(set: set, packs: packs, keywords: keywords, documents: documents)) case .stickerSetNotModified: return nil } } else { return nil } }) } default: break } } return combineLatest(missingSignals) |> map { results -> [Api.StickerSetCovered] in var updatedSets = stickerSets for result in results { if let result = result { updatedSets[result.0] = result.1 } } return updatedSets } } func updatedFeaturedStickerPacks(network: Network, postbox: Postbox, category: FeaturedStickerPacksCategory) -> Signal { return postbox.transaction { transaction -> Signal in let initialPacks = transaction.getOrderedListItems(collectionId: category.itemListNamespace) var initialPackMap: [Int64: FeaturedStickerPackItem] = [:] var unreadIds: [Int64] = [] for entry in initialPacks { let item = entry.contents.get(FeaturedStickerPackItem.self)! initialPackMap[FeaturedStickerPackItemId(entry.id).packId] = item if item.unread { unreadIds.append(item.info.id.id) } } let initialPackIds = initialPacks.map { return FeaturedStickerPackItemId($0.id).packId } let initialHash: Int64 = hashForIdsReverse(initialPackIds, unreadIds: unreadIds) struct FeaturedListContent { var unreadIds: Set var packs: [FeaturedStickerPackItem] var isPremium: Bool } enum FeaturedList { case notModified case content(FeaturedListContent) } let signal: Signal switch category { case .stickerPacks: signal = network.request(Api.functions.messages.getFeaturedStickers(hash: initialHash)) |> mapToSignal { result -> Signal in switch result { case .featuredStickersNotModified: return .single(.notModified) case let .featuredStickers(flags, _, _, sets, unread): return resolveMissingStickerSets(network: network, postbox: postbox, stickerSets: sets, ignorePacksWithHashes: initialPackMap.filter { $0.value.topItems.count > 1 }.mapValues({ item in item.info.hash })) |> castError(MTRpcError.self) |> map { sets -> FeaturedList in let unreadIds = Set(unread) var updatedPacks: [FeaturedStickerPackItem] = [] for set in sets { var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace) if let previousPack = initialPackMap[info.id.id] { if previousPack.info.hash == info.hash, previousPack.topItems.count > 1 { items = previousPack.topItems } else { items = Array(items.prefix(5)) } } updatedPacks.append(FeaturedStickerPackItem(info: StickerPackCollectionInfo.Accessor(info), topItems: items, unread: unreadIds.contains(info.id.id))) } let isPremium = flags & (1 << 0) != 0 return .content(FeaturedListContent( unreadIds: unreadIds, packs: updatedPacks, isPremium: isPremium )) } } } |> `catch` { _ -> Signal in return .single(.notModified) } case .emojiPacks: signal = network.request(Api.functions.messages.getFeaturedEmojiStickers(hash: initialHash)) |> mapToSignal { result -> Signal in switch result { case .featuredStickersNotModified: return .single(.notModified) case let .featuredStickers(flags, _, _, sets, unread): return resolveMissingStickerSets(network: network, postbox: postbox, stickerSets: sets, ignorePacksWithHashes: initialPackMap.mapValues({ item in item.info.hash })) |> castError(MTRpcError.self) |> map { sets -> FeaturedList in let unreadIds = Set(unread) var updatedPacks: [FeaturedStickerPackItem] = [] for set in sets { var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace) if let previousPack = initialPackMap[info.id.id] { if previousPack.info.hash == info.hash { items = previousPack.topItems } } updatedPacks.append(FeaturedStickerPackItem(info: StickerPackCollectionInfo.Accessor(info), topItems: items, unread: unreadIds.contains(info.id.id))) } let isPremium = flags & (1 << 0) != 0 return .content(FeaturedListContent( unreadIds: unreadIds, packs: updatedPacks, isPremium: isPremium )) } } } |> `catch` { _ -> Signal in return .single(.notModified) } } return signal |> mapToSignal { result -> Signal in return postbox.transaction { transaction -> Void in switch result { case .notModified: break case let .content(content): transaction.replaceOrderedItemListItems(collectionId: category.itemListNamespace, items: content.packs.compactMap { item -> OrderedItemListEntry? in if let entry = CodableEntry(item) { return OrderedItemListEntry(id: FeaturedStickerPackItemId(item.info.id.id).rawValue, contents: entry) } else { return nil } }) if let entry = CodableEntry(FeaturedStickersConfiguration(isPremium: content.isPremium)) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, key: ValueBoxKey(length: 0)), entry: entry) } } } } } |> switchToLatest } public func requestOldFeaturedStickerPacks(network: Network, postbox: Postbox, offset: Int, limit: Int) -> Signal<[FeaturedStickerPackItem], NoError> { return network.request(Api.functions.messages.getOldFeaturedStickers(offset: Int32(offset), limit: Int32(limit), hash: 0)) |> retryRequest |> map { result -> [FeaturedStickerPackItem] in switch result { case .featuredStickersNotModified: return [] case let .featuredStickers(_, _, _, sets, unread): let unreadIds = Set(unread) var updatedPacks: [FeaturedStickerPackItem] = [] for set in sets { let (info, items) = parsePreviewStickerSet(set, namespace: Namespaces.ItemCollection.CloudStickerPacks) updatedPacks.append(FeaturedStickerPackItem(info: StickerPackCollectionInfo.Accessor(info), topItems: items, unread: unreadIds.contains(info.id.id))) } return updatedPacks } } } public func preloadedFeaturedStickerSet(network: Network, postbox: Postbox, id: ItemCollectionId) -> Signal { return postbox.transaction { transaction -> Signal in if let pack = transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks, itemId: FeaturedStickerPackItemId(id.id).rawValue)?.contents.get(FeaturedStickerPackItem.self) { if pack.topItems.count < 5 && pack.topItems.count < pack.info.count { return _internal_requestStickerSet(postbox: postbox, network: network, reference: .id(id: pack.info.id.id, accessHash: pack.info.accessHash)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal in if let result = result { return postbox.transaction { transaction -> Void in if let pack = transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks, itemId: FeaturedStickerPackItemId(id.id).rawValue)?.contents.get(FeaturedStickerPackItem.self) { var items = result.items.map({ $0 as? StickerPackItem }).compactMap({ $0 }) if items.count > 5 { items.removeSubrange(5 ..< items.count) } if let entry = CodableEntry(FeaturedStickerPackItem(info: pack.info, topItems: items, unread: pack.unread)) { transaction.updateOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks, itemId: FeaturedStickerPackItemId(id.id).rawValue, item: entry) } } } } else { return .complete() } } } } return .complete() } |> switchToLatest } func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollectionId.Namespace) -> (StickerPackCollectionInfo, [StickerPackItem]) { switch set { case let .stickerSetCovered(set, cover): let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var items: [StickerPackItem] = [] if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id { items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])) } return (info, items) case let .stickerSetMultiCovered(set, covers): let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var items: [StickerPackItem] = [] for cover in covers { if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id { items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])) } } return (info, items) case let .stickerSetFullCovered(set, packs, keywords, documents): 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) } } break } } for keyword in keywords { switch keyword { case let .stickerKeyword(documentId, texts): for text in texts { let key = ValueBoxKey(text).toMemoryBuffer() let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId) if indexKeysByFile[mediaId] == nil { indexKeysByFile[mediaId] = [key] } else { indexKeysByFile[mediaId]!.append(key) } } } } let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) var items: [StickerPackItem] = [] for document in documents { if let file = telegramMediaFileFromApiDocument(document, altDocuments: []), let id = file.id { let fileIndexKeys: [MemoryBuffer] if let indexKeys = indexKeysByFile[id] { fileIndexKeys = indexKeys } else { fileIndexKeys = [] } items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: fileIndexKeys)) } } return (info, items) case let .stickerSetNoCovered(set): let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) let items: [StickerPackItem] = [] return (info, items) } }