import Foundation import Postbox import MtProtoKit import SwiftSignalKit import TelegramApi public final class StarGiftsList: Codable, Equatable { public let items: [StarGift] public let hashValue: Int32 public init(items: [StarGift], hashValue: Int32) { self.items = items self.hashValue = hashValue } public static func ==(lhs: StarGiftsList, rhs: StarGiftsList) -> Bool { if lhs === rhs { return true } if lhs.items != rhs.items { return false } if lhs.hashValue != rhs.hashValue { return false } return true } } public enum StarGift: Equatable, Codable, PostboxCoding { enum CodingKeys: String, CodingKey { case type case value } public struct Gift: Equatable, Codable, PostboxCoding { public struct Flags: OptionSet { public var rawValue: Int32 public init(rawValue: Int32) { self.rawValue = rawValue } public static let isBirthdayGift = Flags(rawValue: 1 << 0) } enum CodingKeys: String, CodingKey { case id case file case price case convertStars case availability case soldOut case flags case upgradeStars } public struct Availability: Equatable, Codable, PostboxCoding { enum CodingKeys: String, CodingKey { case remains case total } public let remains: Int32 public let total: Int32 public init(remains: Int32, total: Int32) { self.remains = remains self.total = total } public init(decoder: PostboxDecoder) { self.remains = decoder.decodeInt32ForKey(CodingKeys.remains.rawValue, orElse: 0) self.total = decoder.decodeInt32ForKey(CodingKeys.total.rawValue, orElse: 0) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.remains, forKey: CodingKeys.remains.rawValue) encoder.encodeInt32(self.total, forKey: CodingKeys.total.rawValue) } } public struct SoldOut: Equatable, Codable, PostboxCoding { enum CodingKeys: String, CodingKey { case firstSale case lastSale } public let firstSale: Int32 public let lastSale: Int32 public init(firstSale: Int32, lastSale: Int32) { self.firstSale = firstSale self.lastSale = lastSale } public init(decoder: PostboxDecoder) { self.firstSale = decoder.decodeInt32ForKey(CodingKeys.firstSale.rawValue, orElse: 0) self.lastSale = decoder.decodeInt32ForKey(CodingKeys.lastSale.rawValue, orElse: 0) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.firstSale, forKey: CodingKeys.firstSale.rawValue) encoder.encodeInt32(self.lastSale, forKey: CodingKeys.lastSale.rawValue) } } public enum DecodingError: Error { case generic } public let id: Int64 public let file: TelegramMediaFile public let price: Int64 public let convertStars: Int64 public let availability: Availability? public let soldOut: SoldOut? public let flags: Flags public let upgradeStars: Int64? public init(id: Int64, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?, flags: Flags, upgradeStars: Int64?) { self.id = id self.file = file self.price = price self.convertStars = convertStars self.availability = availability self.soldOut = soldOut self.flags = flags self.upgradeStars = upgradeStars } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int64.self, forKey: .id) if let fileData = try container.decodeIfPresent(Data.self, forKey: .file), let file = PostboxDecoder(buffer: MemoryBuffer(data: fileData)).decodeRootObject() as? TelegramMediaFile { self.file = file } else { throw DecodingError.generic } self.price = try container.decode(Int64.self, forKey: .price) self.convertStars = try container.decodeIfPresent(Int64.self, forKey: .convertStars) ?? 0 self.availability = try container.decodeIfPresent(Availability.self, forKey: .availability) self.soldOut = try container.decodeIfPresent(SoldOut.self, forKey: .soldOut) self.flags = Flags(rawValue: try container .decodeIfPresent(Int32.self, forKey: .flags) ?? 0) self.upgradeStars = try container.decodeIfPresent(Int64.self, forKey: .upgradeStars) } public init(decoder: PostboxDecoder) { self.id = decoder.decodeInt64ForKey(CodingKeys.id.rawValue, orElse: 0) self.file = decoder.decodeObjectForKey(CodingKeys.file.rawValue) as! TelegramMediaFile self.price = decoder.decodeInt64ForKey(CodingKeys.price.rawValue, orElse: 0) self.convertStars = decoder.decodeInt64ForKey(CodingKeys.convertStars.rawValue, orElse: 0) self.availability = decoder.decodeObjectForKey(CodingKeys.availability.rawValue, decoder: { StarGift.Gift.Availability(decoder: $0) }) as? StarGift.Gift.Availability self.soldOut = decoder.decodeObjectForKey(CodingKeys.soldOut.rawValue, decoder: { StarGift.Gift.SoldOut(decoder: $0) }) as? StarGift.Gift.SoldOut self.flags = Flags(rawValue: decoder.decodeInt32ForKey(CodingKeys.flags.rawValue, orElse: 0)) self.upgradeStars = decoder.decodeOptionalInt64ForKey(CodingKeys.upgradeStars.rawValue) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.id, forKey: .id) let encoder = PostboxEncoder() encoder.encodeRootObject(self.file) let fileData = encoder.makeData() try container.encode(fileData, forKey: .file) try container.encode(self.price, forKey: .price) try container.encode(self.convertStars, forKey: .convertStars) try container.encodeIfPresent(self.availability, forKey: .availability) try container.encodeIfPresent(self.soldOut, forKey: .soldOut) try container.encode(self.flags.rawValue, forKey: .flags) try container.encodeIfPresent(self.upgradeStars, forKey: .upgradeStars) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt64(self.id, forKey: CodingKeys.id.rawValue) encoder.encodeObject(self.file, forKey: CodingKeys.file.rawValue) encoder.encodeInt64(self.price, forKey: CodingKeys.price.rawValue) encoder.encodeInt64(self.convertStars, forKey: CodingKeys.convertStars.rawValue) if let availability = self.availability { encoder.encodeObject(availability, forKey: CodingKeys.availability.rawValue) } else { encoder.encodeNil(forKey: CodingKeys.availability.rawValue) } if let soldOut = self.soldOut { encoder.encodeObject(soldOut, forKey: CodingKeys.soldOut.rawValue) } else { encoder.encodeNil(forKey: CodingKeys.soldOut.rawValue) } encoder.encodeInt32(self.flags.rawValue, forKey: CodingKeys.flags.rawValue) if let upgradeStars = self.upgradeStars { encoder.encodeInt64(upgradeStars, forKey: CodingKeys.upgradeStars.rawValue) } else { encoder.encodeNil(forKey: CodingKeys.upgradeStars.rawValue) } } } public struct UniqueGift: Equatable, Codable, PostboxCoding { enum CodingKeys: String, CodingKey { case id case title case number case slug case ownerPeerId case ownerName case ownerAddress case attributes case availability case giftAddress } public enum Attribute: Equatable, Codable, PostboxCoding { enum CodingKeys: String, CodingKey { case type case name case file case innerColor case outerColor case patternColor case textColor case sendPeerId case recipientPeerId case date case text case entities case rarity } public enum AttributeType { case model case pattern case backdrop case originalInfo } case model(name: String, file: TelegramMediaFile, rarity: Int32) case pattern(name: String, file: TelegramMediaFile, rarity: Int32) case backdrop(name: String, innerColor: Int32, outerColor: Int32, patternColor: Int32, textColor: Int32, rarity: Int32) case originalInfo(senderPeerId: EnginePeer.Id?, recipientPeerId: EnginePeer.Id, date: Int32, text: String?, entities: [MessageTextEntity]?) public var attributeType: AttributeType { switch self { case .model: return .model case .pattern: return .pattern case .backdrop: return .backdrop case .originalInfo: return .originalInfo } } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(Int32.self, forKey: .type) switch type { case 0: self = .model( name: try container.decode(String.self, forKey: .name), file: try container.decode(TelegramMediaFile.self, forKey: .file), rarity: try container.decode(Int32.self, forKey: .rarity) ) case 1: self = .pattern( name: try container.decode(String.self, forKey: .name), file: try container.decode(TelegramMediaFile.self, forKey: .file), rarity: try container.decode(Int32.self, forKey: .rarity) ) case 2: self = .backdrop( name: try container.decode(String.self, forKey: .name), innerColor: try container.decode(Int32.self, forKey: .innerColor), outerColor: try container.decode(Int32.self, forKey: .outerColor), patternColor: try container.decode(Int32.self, forKey: .patternColor), textColor: try container.decode(Int32.self, forKey: .textColor), rarity: try container.decode(Int32.self, forKey: .rarity) ) case 3: self = .originalInfo( senderPeerId: try container.decodeIfPresent(Int64.self, forKey: .sendPeerId).flatMap { EnginePeer.Id($0) }, recipientPeerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .recipientPeerId)), date: try container.decode(Int32.self, forKey: .date), text: try container.decodeIfPresent(String.self, forKey: .text), entities: try container.decodeIfPresent([MessageTextEntity].self, forKey: .entities) ) default: throw DecodingError.generic } } public init(decoder: PostboxDecoder) { let type = decoder.decodeInt32ForKey(CodingKeys.type.rawValue, orElse: 0) switch type { case 0: self = .model( name: decoder.decodeStringForKey(CodingKeys.name.rawValue, orElse: ""), file: decoder.decodeObjectForKey(CodingKeys.file.rawValue) as! TelegramMediaFile, rarity: decoder.decodeInt32ForKey(CodingKeys.rarity.rawValue, orElse: 0) ) case 1: self = .pattern( name: decoder.decodeStringForKey(CodingKeys.name.rawValue, orElse: ""), file: decoder.decodeObjectForKey(CodingKeys.file.rawValue) as! TelegramMediaFile, rarity: decoder.decodeInt32ForKey(CodingKeys.rarity.rawValue, orElse: 0) ) case 2: self = .backdrop( name: decoder.decodeStringForKey(CodingKeys.name.rawValue, orElse: ""), innerColor: decoder.decodeInt32ForKey(CodingKeys.innerColor.rawValue, orElse: 0), outerColor: decoder.decodeInt32ForKey(CodingKeys.outerColor.rawValue, orElse: 0), patternColor: decoder.decodeInt32ForKey(CodingKeys.patternColor.rawValue, orElse: 0), textColor: decoder.decodeInt32ForKey(CodingKeys.textColor.rawValue, orElse: 0), rarity: decoder.decodeInt32ForKey(CodingKeys.rarity.rawValue, orElse: 0) ) case 3: self = .originalInfo( senderPeerId: decoder.decodeOptionalInt64ForKey(CodingKeys.sendPeerId.rawValue).flatMap { EnginePeer.Id($0) }, recipientPeerId: EnginePeer.Id(decoder.decodeInt64ForKey(CodingKeys.recipientPeerId.rawValue, orElse: 0)), date: decoder.decodeInt32ForKey(CodingKeys.date.rawValue, orElse: 0), text: decoder.decodeOptionalStringForKey(CodingKeys.text.rawValue), entities: decoder.decodeObjectArrayWithDecoderForKey(CodingKeys.entities.rawValue) ) default: fatalError() } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case let .model(name, file, rarity): try container.encode(Int32(0), forKey: .type) try container.encode(name, forKey: .name) try container.encode(file, forKey: .file) try container.encode(rarity, forKey: .rarity) case let .pattern(name, file, rarity): try container.encode(Int32(1), forKey: .type) try container.encode(name, forKey: .name) try container.encode(file, forKey: .file) try container.encode(rarity, forKey: .rarity) case let .backdrop(name, innerColor, outerColor, patternColor, textColor, rarity): try container.encode(Int32(2), forKey: .type) try container.encode(name, forKey: .name) try container.encode(innerColor, forKey: .innerColor) try container.encode(outerColor, forKey: .outerColor) try container.encode(patternColor, forKey: .patternColor) try container.encode(textColor, forKey: .textColor) try container.encode(rarity, forKey: .rarity) case let .originalInfo(senderPeerId, recipientPeerId, date, text, entities): try container.encode(Int32(3), forKey: .type) try container.encodeIfPresent(senderPeerId?.toInt64(), forKey: .sendPeerId) try container.encode(recipientPeerId.toInt64(), forKey: .recipientPeerId) try container.encode(date, forKey: .date) try container.encodeIfPresent(text, forKey: .text) try container.encodeIfPresent(entities, forKey: .entities) } } public func encode(_ encoder: PostboxEncoder) { switch self { case let .model(name, file, rarity): encoder.encodeInt32(0, forKey: CodingKeys.type.rawValue) encoder.encodeString(name, forKey: CodingKeys.name.rawValue) encoder.encodeObject(file, forKey: CodingKeys.file.rawValue) encoder.encodeInt32(rarity, forKey: CodingKeys.rarity.rawValue) case let .pattern(name, file, rarity): encoder.encodeInt32(1, forKey: CodingKeys.type.rawValue) encoder.encodeString(name, forKey: CodingKeys.name.rawValue) encoder.encodeObject(file, forKey: CodingKeys.file.rawValue) encoder.encodeInt32(rarity, forKey: CodingKeys.rarity.rawValue) case let .backdrop(name, innerColor, outerColor, patternColor, textColor, rarity): encoder.encodeInt32(2, forKey: CodingKeys.type.rawValue) encoder.encodeString(name, forKey: CodingKeys.name.rawValue) encoder.encodeInt32(innerColor, forKey: CodingKeys.innerColor.rawValue) encoder.encodeInt32(outerColor, forKey: CodingKeys.outerColor.rawValue) encoder.encodeInt32(patternColor, forKey: CodingKeys.patternColor.rawValue) encoder.encodeInt32(textColor, forKey: CodingKeys.textColor.rawValue) encoder.encodeInt32(rarity, forKey: CodingKeys.rarity.rawValue) case let .originalInfo(senderPeerId, recipientPeerId, date, text, entities): encoder.encodeInt32(3, forKey: CodingKeys.type.rawValue) if let senderPeerId { encoder.encodeInt64(senderPeerId.toInt64(), forKey: CodingKeys.sendPeerId.rawValue) } else { encoder.encodeNil(forKey: CodingKeys.sendPeerId.rawValue) } encoder.encodeInt64(recipientPeerId.toInt64(), forKey: CodingKeys.recipientPeerId.rawValue) encoder.encodeInt32(date, forKey: CodingKeys.date.rawValue) if let text { encoder.encodeString(text, forKey: CodingKeys.text.rawValue) if let entities { encoder.encodeObjectArray(entities, forKey: CodingKeys.entities.rawValue) } else { encoder.encodeNil(forKey: CodingKeys.entities.rawValue) } } else { encoder.encodeNil(forKey: CodingKeys.text.rawValue) encoder.encodeNil(forKey: CodingKeys.entities.rawValue) } } } } public struct Availability: Equatable, Codable, PostboxCoding { enum CodingKeys: String, CodingKey { case issued case total } public let issued: Int32 public let total: Int32 public init(issued: Int32, total: Int32) { self.issued = issued self.total = total } public init(decoder: PostboxDecoder) { self.issued = decoder.decodeInt32ForKey(CodingKeys.issued.rawValue, orElse: 0) self.total = decoder.decodeInt32ForKey(CodingKeys.total.rawValue, orElse: 0) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.issued, forKey: CodingKeys.issued.rawValue) encoder.encodeInt32(self.total, forKey: CodingKeys.total.rawValue) } } public enum Owner: Equatable { case peerId(EnginePeer.Id) case name(String) case address(String) } public enum DecodingError: Error { case generic } public let id: Int64 public let title: String public let number: Int32 public let slug: String public let owner: Owner public let attributes: [Attribute] public let availability: Availability public let giftAddress: String? public init(id: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?) { self.id = id self.title = title self.number = number self.slug = slug self.owner = owner self.attributes = attributes self.availability = availability self.giftAddress = giftAddress } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int64.self, forKey: .id) self.title = try container.decode(String.self, forKey: .title) self.number = try container.decode(Int32.self, forKey: .number) self.slug = try container.decodeIfPresent(String.self, forKey: .slug) ?? "" if let ownerId = try container.decodeIfPresent(Int64.self, forKey: .ownerPeerId) { self.owner = .peerId(EnginePeer.Id(ownerId)) } else if let ownerAddress = try container.decodeIfPresent(String.self, forKey: .ownerAddress) { self.owner = .address(ownerAddress) } else if let ownerName = try container.decodeIfPresent(String.self, forKey: .ownerName) { self.owner = .name(ownerName) } else { self.owner = .name("Unknown") } self.attributes = try container.decode([UniqueGift.Attribute].self, forKey: .attributes) self.availability = try container.decode(UniqueGift.Availability.self, forKey: .availability) self.giftAddress = try container.decodeIfPresent(String.self, forKey: .giftAddress) } public init(decoder: PostboxDecoder) { self.id = decoder.decodeInt64ForKey(CodingKeys.id.rawValue, orElse: 0) self.title = decoder.decodeStringForKey(CodingKeys.title.rawValue, orElse: "") self.number = decoder.decodeInt32ForKey(CodingKeys.number.rawValue, orElse: 0) self.slug = decoder.decodeStringForKey(CodingKeys.slug.rawValue, orElse: "") if let ownerId = decoder.decodeOptionalInt64ForKey(CodingKeys.ownerPeerId.rawValue) { self.owner = .peerId(EnginePeer.Id(ownerId)) } else if let ownerAddress = decoder.decodeOptionalStringForKey(CodingKeys.ownerAddress.rawValue) { self.owner = .address(ownerAddress) } else if let ownerName = decoder.decodeOptionalStringForKey(CodingKeys.ownerName.rawValue) { self.owner = .name(ownerName) } else { self.owner = .name("Unknown") } self.attributes = (try? decoder.decodeObjectArrayWithCustomDecoderForKey(CodingKeys.attributes.rawValue, decoder: { UniqueGift.Attribute(decoder: $0) })) ?? [] self.availability = decoder.decodeObjectForKey(CodingKeys.availability.rawValue, decoder: { UniqueGift.Availability(decoder: $0) }) as! UniqueGift.Availability self.giftAddress = decoder.decodeOptionalStringForKey(CodingKeys.giftAddress.rawValue) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.id, forKey: .id) try container.encode(self.title, forKey: .title) try container.encode(self.number, forKey: .number) try container.encode(self.slug, forKey: .slug) switch self.owner { case let .peerId(peerId): try container.encode(peerId.toInt64(), forKey: .ownerPeerId) case let .name(name): try container.encode(name, forKey: .ownerName) case let .address(address): try container.encode(address, forKey: .ownerAddress) } try container.encode(self.attributes, forKey: .attributes) try container.encode(self.availability, forKey: .availability) try container.encodeIfPresent(self.giftAddress, forKey: .giftAddress) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt64(self.id, forKey: CodingKeys.id.rawValue) encoder.encodeString(self.title, forKey: CodingKeys.title.rawValue) encoder.encodeInt32(self.number, forKey: CodingKeys.number.rawValue) encoder.encodeString(self.slug, forKey: CodingKeys.slug.rawValue) switch self.owner { case let .peerId(peerId): encoder.encodeInt64(peerId.toInt64(), forKey: CodingKeys.ownerPeerId.rawValue) case let .name(name): encoder.encodeString(name, forKey: CodingKeys.ownerName.rawValue) case let .address(address): encoder.encodeString(address, forKey: CodingKeys.ownerAddress.rawValue) } encoder.encodeObjectArray(self.attributes, forKey: CodingKeys.attributes.rawValue) encoder.encodeObject(self.availability, forKey: CodingKeys.availability.rawValue) if let giftAddress = self.giftAddress { encoder.encodeString(giftAddress, forKey: CodingKeys.giftAddress.rawValue) } else { encoder.encodeNil(forKey: CodingKeys.giftAddress.rawValue) } } } public enum DecodingError: Error { case generic } case generic(StarGift.Gift) case unique(StarGift.UniqueGift) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(Int32.self, forKey: .type) switch type { case 0: self = .generic(try container.decode(StarGift.Gift.self, forKey: .value)) case 1: self = .unique(try container.decode(StarGift.UniqueGift.self, forKey: .value)) default: throw DecodingError.generic } } public init(decoder: PostboxDecoder) { let type = decoder.decodeInt32ForKey(CodingKeys.type.rawValue, orElse: -1) switch type { case -1: self = .generic(Gift(decoder: decoder)) case 0: self = .generic(decoder.decodeObjectForKey(CodingKeys.value.rawValue, decoder: { StarGift.Gift(decoder: $0) }) as! StarGift.Gift) case 1: self = .unique(decoder.decodeObjectForKey(CodingKeys.value.rawValue, decoder: { StarGift.UniqueGift(decoder: $0) }) as! StarGift.UniqueGift) default: fatalError() } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case let .generic(gift): try container.encode(Int32(0), forKey: .type) try container.encode(gift, forKey: .value) case let .unique(uniqueGift): try container.encode(Int32(1), forKey: .type) try container.encode(uniqueGift, forKey: .value) } } public func encode(_ encoder: PostboxEncoder) { switch self { case let .generic(gift): encoder.encodeInt32(0, forKey: CodingKeys.type.rawValue) encoder.encodeObject(gift, forKey: CodingKeys.value.rawValue) case let .unique(uniqueGift): encoder.encodeInt32(1, forKey: CodingKeys.type.rawValue) encoder.encodeObject(uniqueGift, forKey: CodingKeys.value.rawValue) } } } extension StarGift { init?(apiStarGift: Api.StarGift) { switch apiStarGift { case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, convertStars, firstSale, lastSale, upgradeStars): var flags = StarGift.Gift.Flags() if (apiFlags & (1 << 2)) != 0 { flags.insert(.isBirthdayGift) } var availability: StarGift.Gift.Availability? if let availabilityRemains, let availabilityTotal { availability = StarGift.Gift.Availability(remains: availabilityRemains, total: availabilityTotal) } var soldOut: StarGift.Gift.SoldOut? if let firstSale, let lastSale { soldOut = StarGift.Gift.SoldOut(firstSale: firstSale, lastSale: lastSale) } guard let file = telegramMediaFileFromApiDocument(sticker, altDocuments: nil) else { return nil } self = .generic(StarGift.Gift(id: id, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars)) case let .starGiftUnique(_, id, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress): let owner: StarGift.UniqueGift.Owner if let ownerAddress { owner = .address(ownerAddress) } else if let ownerId = ownerPeerId?.peerId { owner = .peerId(ownerId) } else if let ownerName { owner = .name(ownerName) } else { return nil } self = .unique(StarGift.UniqueGift(id: id, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress)) } } } func _internal_cachedStarGifts(postbox: Postbox) -> Signal { let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.starGifts()])) return postbox.combinedView(keys: [viewKey]) |> map { views -> StarGiftsList? in guard let view = views.views[viewKey] as? PreferencesView else { return nil } guard let value = view.values[PreferencesKeys.starGifts()]?.get(StarGiftsList.self) else { return nil } return value } } func _internal_keepCachedStarGiftsUpdated(postbox: Postbox, network: Network) -> Signal { let updateSignal = _internal_cachedStarGifts(postbox: postbox) |> take(1) |> mapToSignal { list -> Signal in return network.request(Api.functions.payments.getStarGifts(hash: 0)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal in guard let result else { return .complete() } return postbox.transaction { transaction in switch result { case let .starGifts(hash, gifts): let starGiftsLists = StarGiftsList(items: gifts.compactMap { StarGift(apiStarGift: $0) }, hashValue: hash) transaction.setPreferencesEntry(key: PreferencesKeys.starGifts(), value: PreferencesEntry(starGiftsLists)) case .starGiftsNotModified: break } } |> ignoreValues } } return updateSignal } func managedStarGiftsUpdates(postbox: Postbox, network: Network) -> Signal { let poll = _internal_keepCachedStarGiftsUpdated(postbox: postbox, network: network) return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } func _internal_convertStarGift(account: Account, reference: StarGiftReference) -> Signal { return account.postbox.transaction { transaction in return reference.apiStarGiftReference(transaction: transaction) } |> mapToSignal { starGift in guard let starGift else { return .complete() } return account.network.request(Api.functions.payments.convertStarGift(stargift: starGift)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result in if let result, case .boolTrue = result { return account.postbox.transaction { transaction -> Void in transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, cachedData -> CachedPeerData? in if let cachedData = cachedData as? CachedUserData, let starGiftsCount = cachedData.starGiftsCount { var updatedData = cachedData updatedData = updatedData.withUpdatedStarGiftsCount(max(0, starGiftsCount - 1)) return updatedData } else { return cachedData } }) } } return .complete() } |> ignoreValues } } func _internal_updateStarGiftAddedToProfile(account: Account, reference: StarGiftReference, added: Bool) -> Signal { var flags: Int32 = 0 if !added { flags |= (1 << 0) } return account.postbox.transaction { transaction in return reference.apiStarGiftReference(transaction: transaction) } |> mapToSignal { starGift in guard let starGift else { return .complete() } return account.network.request(Api.functions.payments.saveStarGift(flags: flags, stargift: starGift)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> ignoreValues } } public enum TransferStarGiftError { case generic } func _internal_transferStarGift(account: Account, prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal { return account.postbox.transaction { transaction -> (Api.InputPeer, Api.InputSavedStarGift)? in guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer), let starGift = reference.apiStarGiftReference(transaction: transaction) else { return nil } return (inputPeer, starGift) } |> castError(TransferStarGiftError.self) |> mapToSignal { inputPeerAndStarGift -> Signal in guard let (inputPeer, starGift) = inputPeerAndStarGift else { return .complete() } if prepaid { return account.network.request(Api.functions.payments.transferStarGift(stargift: starGift, toId: inputPeer)) |> mapError { _ -> TransferStarGiftError in return .generic } |> mapToSignal { updates -> Signal in account.stateManager.addUpdates(updates) return .complete() } |> ignoreValues } else { let source: BotPaymentInvoiceSource = .starGiftTransfer(reference: reference, toPeerId: peerId) return _internal_fetchBotPaymentForm(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, source: source, themeParams: nil) |> map(Optional.init) |> `catch` { error -> Signal in if case .noPaymentNeeded = error { return .single(nil) } return .fail(.generic) } |> mapToSignal { paymentForm in if let paymentForm { return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source) |> mapError { _ -> TransferStarGiftError in return .generic } |> ignoreValues } else { return _internal_transferStarGift(account: account, prepaid: true, reference: reference, peerId: peerId) } } } } } public enum UpgradeStarGiftError { case generic } func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal { if let formId { let source: BotPaymentInvoiceSource = .starGiftUpgrade(keepOriginalInfo: keepOriginalInfo, reference: reference) return _internal_sendStarsPaymentForm(account: account, formId: formId, source: source) |> mapError { _ -> UpgradeStarGiftError in return .generic } |> mapToSignal { result in if case let .done(_, _, gift) = result, let gift { return .single(gift) } else { return .complete() } } } else { var flags: Int32 = 0 if keepOriginalInfo { flags |= (1 << 0) } return account.postbox.transaction { transaction in return reference.apiStarGiftReference(transaction: transaction) } |> castError(UpgradeStarGiftError.self) |> mapToSignal { starGift in guard let starGift else { return .fail(.generic) } return account.network.request(Api.functions.payments.upgradeStarGift(flags: flags, stargift: starGift)) |> mapError { _ -> UpgradeStarGiftError in return .generic } |> mapToSignal { updates in account.stateManager.addUpdates(updates) for update in updates.allUpdates { switch update { case let .updateNewMessage(message, _, _): if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) } else { reference = .message(messageId: messageId) } return .single(ProfileGiftsContext.State.StarGift( gift: gift, reference: reference, fromPeer: nil, date: message.timestamp, text: nil, entities: nil, nameHidden: false, savedToProfile: savedToProfile, convertStars: nil, canUpgrade: false, canExportDate: canExportDate, upgradeStars: nil, transferStars: transferStars )) } } } default: break } } return .fail(.generic) } } } } func _internal_starGiftUpgradePreview(account: Account, giftId: Int64) -> Signal<[StarGift.UniqueGift.Attribute], NoError> { return account.network.request(Api.functions.payments.getStarGiftUpgradePreview(giftId: giftId)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> map { result in guard let result else { return [] } switch result { case let .starGiftUpgradePreview(sampleAttributes): return sampleAttributes.compactMap { StarGift.UniqueGift.Attribute(apiAttribute: $0) } } } } private final class CachedProfileGifts: Codable { enum CodingKeys: String, CodingKey { case gifts case count case notificationsEnabled } var gifts: [ProfileGiftsContext.State.StarGift] let count: Int32 let notificationsEnabled: Bool? init(gifts: [ProfileGiftsContext.State.StarGift], count: Int32, notificationsEnabled: Bool?) { self.gifts = gifts self.count = count self.notificationsEnabled = notificationsEnabled } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.gifts = try container.decode([ProfileGiftsContext.State.StarGift].self, forKey: .gifts) self.count = try container.decode(Int32.self, forKey: .count) self.notificationsEnabled = try container.decodeIfPresent(Bool.self, forKey: .notificationsEnabled) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.gifts, forKey: .gifts) try container.encode(self.count, forKey: .count) try container.encodeIfPresent(self.notificationsEnabled, forKey: .notificationsEnabled) } func render(transaction: Transaction) { for i in 0 ..< self.gifts.count { let gift = self.gifts[i] if gift.fromPeer == nil, let fromPeerId = gift._fromPeerId, let peer = transaction.getPeer(fromPeerId) { self.gifts[i] = gift.withFromPeer(EnginePeer(peer)) } } } } private func entryId(peerId: EnginePeer.Id) -> ItemCacheEntryId { let cacheKey = ValueBoxKey(length: 8) cacheKey.setInt64(0, value: peerId.toInt64()) return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedProfileGifts, key: cacheKey) } private final class ProfileGiftsContextImpl { private let queue: Queue private let account: Account private let peerId: PeerId private let disposable = MetaDisposable() private let cacheDisposable = MetaDisposable() private let actionDisposable = MetaDisposable() private var sorting: ProfileGiftsContext.Sorting = .date private var filter: ProfileGiftsContext.Filters = ProfileGiftsContext.Filters.All private var gifts: [ProfileGiftsContext.State.StarGift] = [] private var count: Int32? private var dataState: ProfileGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil) private var filteredGifts: [ProfileGiftsContext.State.StarGift] = [] private var filteredCount: Int32? private var filteredDataState: ProfileGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil) private var notificationsEnabled: Bool? var _state: ProfileGiftsContext.State? private let stateValue = Promise() var state: Signal { return self.stateValue.get() } init(queue: Queue, account: Account, peerId: EnginePeer.Id) { self.queue = queue self.account = account self.peerId = peerId self.loadMore() } deinit { self.disposable.dispose() self.cacheDisposable.dispose() self.actionDisposable.dispose() } func reload() { self.gifts = [] self.dataState = .ready(canLoadMore: true, nextOffset: nil) self.loadMore(reload: true) } func loadMore(reload: Bool = false) { let peerId = self.peerId let accountPeerId = self.account.peerId let network = self.account.network let postbox = self.account.postbox let filter = self.filter let sorting = self.sorting let isFiltered = self.filter != .All || self.sorting != .date let dataState = isFiltered ? self.filteredDataState : self.dataState if case let .ready(true, initialNextOffset) = dataState { if !isFiltered, self.gifts.isEmpty, initialNextOffset == nil { self.cacheDisposable.set((self.account.postbox.transaction { transaction -> CachedProfileGifts? in let cachedGifts = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedProfileGifts.self) cachedGifts?.render(transaction: transaction) return cachedGifts } |> deliverOn(self.queue)).start(next: { [weak self] cachedGifts in guard let self, let cachedGifts else { return } if case .loading = self.dataState { self.gifts = cachedGifts.gifts self.count = cachedGifts.count self.notificationsEnabled = cachedGifts.notificationsEnabled self.pushState() } })) } if isFiltered { self.filteredDataState = .loading } else { self.dataState = .loading } if !reload { self.pushState() } let signal: Signal<([ProfileGiftsContext.State.StarGift], Int32, String?, Bool?), NoError> = self.account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } |> mapToSignal { inputPeer -> Signal<([ProfileGiftsContext.State.StarGift], Int32, String?, Bool?), NoError> in guard let inputPeer else { return .single(([], 0, nil, nil)) } var flags: Int32 = 0 if case .value = sorting { flags |= (1 << 5) } if !filter.contains(.hidden) { flags |= (1 << 0) } if !filter.contains(.displayed) { flags |= (1 << 1) } if !filter.contains(.unlimited) { flags |= (1 << 2) } if !filter.contains(.limited) { flags |= (1 << 3) } if !filter.contains(.unique) { flags |= (1 << 4) } return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, offset: initialNextOffset ?? "", limit: 32)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal<([ProfileGiftsContext.State.StarGift], Int32, String?, Bool?), NoError> in guard let result else { return .single(([], 0, nil, nil)) } return postbox.transaction { transaction -> ([ProfileGiftsContext.State.StarGift], Int32, String?, Bool?) in switch result { case let .savedStarGifts(_, count, apiNotificationsEnabled, pinnedToTop, apiGifts, nextOffset, chats, users): let _ = pinnedToTop let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) var notificationsEnabled: Bool? if let apiNotificationsEnabled { if case .boolTrue = apiNotificationsEnabled { notificationsEnabled = true } else { notificationsEnabled = false } } let gifts = apiGifts.compactMap { ProfileGiftsContext.State.StarGift(apiSavedStarGift: $0, peerId: peerId, transaction: transaction) } return (gifts, count, nextOffset, notificationsEnabled) } } } } self.disposable.set((signal |> deliverOn(self.queue)).start(next: { [weak self] (gifts, count, nextOffset, notificationsEnabled) in guard let self else { return } if isFiltered { if initialNextOffset == nil || reload { self.filteredGifts = gifts } else { for gift in gifts { self.filteredGifts.append(gift) } } let updatedCount = max(Int32(self.filteredGifts.count), count) self.filteredCount = updatedCount self.filteredDataState = .ready(canLoadMore: count != 0 && updatedCount > self.filteredGifts.count && nextOffset != nil, nextOffset: nextOffset) } else { if initialNextOffset == nil || reload { self.gifts = gifts self.cacheDisposable.set(self.account.postbox.transaction { transaction in if let entry = CodableEntry(CachedProfileGifts(gifts: gifts, count: count, notificationsEnabled: notificationsEnabled)) { transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry) } }.start()) } else { for gift in gifts { self.gifts.append(gift) } } let updatedCount = max(Int32(self.gifts.count), count) self.count = updatedCount self.dataState = .ready(canLoadMore: count != 0 && updatedCount > self.gifts.count && nextOffset != nil, nextOffset: nextOffset) } self.notificationsEnabled = notificationsEnabled self.pushState() })) } } func updateStarGiftAddedToProfile(reference: StarGiftReference, added: Bool) { self.actionDisposable.set( _internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: added).startStrict() ) if let index = self.gifts.firstIndex(where: { $0.reference == reference }) { self.gifts[index] = self.gifts[index].withSavedToProfile(added) } if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }) { self.filteredGifts[index] = self.filteredGifts[index].withSavedToProfile(added) if !self.filter.contains(.hidden) && !added { self.filteredGifts.remove(at: index) } } self.pushState() } func convertStarGift(reference: StarGiftReference) { self.actionDisposable.set( _internal_convertStarGift(account: self.account, reference: reference).startStrict() ) if let count = self.count { self.count = max(0, count - 1) } self.gifts.removeAll(where: { $0.reference == reference }) self.filteredGifts.removeAll(where: { $0.reference == reference }) self.pushState() } func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) { self.actionDisposable.set( _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId).startStrict() ) if let count = self.count { self.count = max(0, count - 1) } self.gifts.removeAll(where: { $0.reference == reference }) self.filteredGifts.removeAll(where: { $0.reference == reference }) self.pushState() } func upgradeStarGift(formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal { return Signal { [weak self] subscriber in guard let self else { return EmptyDisposable } let disposable = MetaDisposable() disposable.set( _internal_upgradeStarGift(account: self.account, formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in guard let self else { return } if let index = self.gifts.firstIndex(where: { $0.reference == reference }) { self.gifts[index] = result } if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }) { self.filteredGifts[index] = result } self.pushState() subscriber.putNext(result) }, error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() }) ) return disposable } } func toggleStarGiftsNotifications(enabled: Bool) { self.actionDisposable.set( _internal_toggleStarGiftsNotifications(account: self.account, peerId: self.peerId, enabled: enabled).startStrict() ) self.notificationsEnabled = enabled self.pushState() } func updateFilter(_ filter: ProfileGiftsContext.Filters) { self.filter = filter self.filteredDataState = .ready(canLoadMore: true, nextOffset: nil) self.pushState() self.loadMore() } func updateSorting(_ sorting: ProfileGiftsContext.Sorting) { self.sorting = sorting self.filteredDataState = .ready(canLoadMore: true, nextOffset: nil) self.pushState() self.loadMore() } private func pushState() { let useMainData = (self.filter == .All && self.sorting == .date) || self.filteredCount == nil let effectiveGifts = useMainData ? self.gifts : self.filteredGifts let effectiveCount = useMainData ? self.count : self.filteredCount let effectiveDataState = useMainData ? self.dataState : self.filteredDataState let state = ProfileGiftsContext.State( filter: self.filter, sorting: self.sorting, gifts: self.gifts, filteredGifts: effectiveGifts, count: effectiveCount, dataState: effectiveDataState, notificationsEnabled: self.notificationsEnabled ) self._state = state self.stateValue.set(.single(state)) } } public final class ProfileGiftsContext { public struct Filters: OptionSet { public var rawValue: Int32 public init(rawValue: Int32) { self.rawValue = rawValue } public static let unlimited = Filters(rawValue: 1 << 0) public static let limited = Filters(rawValue: 1 << 1) public static let unique = Filters(rawValue: 1 << 2) public static let displayed = Filters(rawValue: 1 << 3) public static let hidden = Filters(rawValue: 1 << 4) public static var All: Filters { return [.unlimited, .limited, .unique, .displayed, .hidden] } } public enum Sorting: Equatable { case date case value } public struct State: Equatable { public struct StarGift: Equatable, Codable { enum CodingKeys: String, CodingKey { case gift case reference case fromPeerId case date case text case entities case messageId case nameHidden case savedToProfile case convertStars case canUpgrade case canExportDate case upgradeStars case transferStars case giftAddress } public let gift: TelegramCore.StarGift public let reference: StarGiftReference? public let fromPeer: EnginePeer? public let date: Int32 public let text: String? public let entities: [MessageTextEntity]? public let nameHidden: Bool public let savedToProfile: Bool public let convertStars: Int64? public let canUpgrade: Bool public let canExportDate: Int32? public let upgradeStars: Int64? public let transferStars: Int64? fileprivate let _fromPeerId: EnginePeer.Id? public enum DecodingError: Error { case generic } public init ( gift: TelegramCore.StarGift, reference: StarGiftReference?, fromPeer: EnginePeer?, date: Int32, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, convertStars: Int64?, canUpgrade: Bool, canExportDate: Int32?, upgradeStars: Int64?, transferStars: Int64? ) { self.gift = gift self.reference = reference self.fromPeer = fromPeer self._fromPeerId = fromPeer?.id self.date = date self.text = text self.entities = entities self.nameHidden = nameHidden self.savedToProfile = savedToProfile self.convertStars = convertStars self.canUpgrade = canUpgrade self.canExportDate = canExportDate self.upgradeStars = upgradeStars self.transferStars = transferStars } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.gift = try container.decode(TelegramCore.StarGift.self, forKey: .gift) if let reference = try container.decodeIfPresent(StarGiftReference.self, forKey: .reference) { self.reference = reference } else if let messageId = try container.decodeIfPresent(EngineMessage.Id.self, forKey: .messageId) { self.reference = .message(messageId: messageId) } else { self.reference = nil } self.fromPeer = nil self._fromPeerId = try container.decodeIfPresent(EnginePeer.Id.self, forKey: .fromPeerId) self.date = try container.decode(Int32.self, forKey: .date) self.text = try container.decodeIfPresent(String.self, forKey: .text) self.entities = try container.decodeIfPresent([MessageTextEntity].self, forKey: .entities) self.nameHidden = try container.decode(Bool.self, forKey: .nameHidden) self.savedToProfile = try container.decode(Bool.self, forKey: .savedToProfile) self.convertStars = try container.decodeIfPresent(Int64.self, forKey: .convertStars) self.canUpgrade = try container.decode(Bool.self, forKey: .canUpgrade) self.canExportDate = try container.decodeIfPresent(Int32.self, forKey: .canExportDate) self.upgradeStars = try container.decodeIfPresent(Int64.self, forKey: .upgradeStars) self.transferStars = try container.decodeIfPresent(Int64.self, forKey: .transferStars) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.gift, forKey: .gift) try container.encodeIfPresent(self.reference, forKey: .reference) try container.encodeIfPresent(self.fromPeer?.id, forKey: .fromPeerId) try container.encode(self.date, forKey: .date) try container.encodeIfPresent(self.text, forKey: .text) try container.encodeIfPresent(self.entities, forKey: .entities) try container.encode(self.nameHidden, forKey: .nameHidden) try container.encode(self.savedToProfile, forKey: .savedToProfile) try container.encodeIfPresent(self.convertStars, forKey: .convertStars) try container.encode(self.canUpgrade, forKey: .canUpgrade) try container.encodeIfPresent(self.canExportDate, forKey: .canExportDate) try container.encodeIfPresent(self.upgradeStars, forKey: .upgradeStars) try container.encodeIfPresent(self.transferStars, forKey: .transferStars) } public func withSavedToProfile(_ savedToProfile: Bool) -> StarGift { return StarGift( gift: self.gift, reference: self.reference, fromPeer: self.fromPeer, date: self.date, text: self.text, entities: self.entities, nameHidden: self.nameHidden, savedToProfile: savedToProfile, convertStars: self.convertStars, canUpgrade: self.canUpgrade, canExportDate: self.canExportDate, upgradeStars: self.upgradeStars, transferStars: self.transferStars ) } fileprivate func withFromPeer(_ fromPeer: EnginePeer?) -> StarGift { return StarGift( gift: self.gift, reference: self.reference, fromPeer: fromPeer, date: self.date, text: self.text, entities: self.entities, nameHidden: self.nameHidden, savedToProfile: savedToProfile, convertStars: self.convertStars, canUpgrade: self.canUpgrade, canExportDate: self.canExportDate, upgradeStars: self.upgradeStars, transferStars: self.transferStars ) } } public enum DataState: Equatable { case loading case ready(canLoadMore: Bool, nextOffset: String?) } public var filter: Filters public var sorting: Sorting public var gifts: [ProfileGiftsContext.State.StarGift] public var filteredGifts: [ProfileGiftsContext.State.StarGift] public var count: Int32? public var dataState: ProfileGiftsContext.State.DataState public var notificationsEnabled: Bool? } private let queue: Queue = .mainQueue() private let impl: QueueLocalObject public var state: Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.state.start(next: { value in subscriber.putNext(value) })) } return disposable } } public init(account: Account, peerId: EnginePeer.Id) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { return ProfileGiftsContextImpl(queue: queue, account: account, peerId: peerId) }) } public func loadMore() { self.impl.with { impl in impl.loadMore() } } public func reload() { self.impl.with { impl in impl.reload() } } public func updateStarGiftAddedToProfile(reference: StarGiftReference, added: Bool) { self.impl.with { impl in impl.updateStarGiftAddedToProfile(reference: reference, added: added) } } public func convertStarGift(reference: StarGiftReference) { self.impl.with { impl in impl.convertStarGift(reference: reference) } } public func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) { self.impl.with { impl in impl.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId) } } public func upgradeStarGift(formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo).start(next: { value in subscriber.putNext(value) }, error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() })) } return disposable } } public func toggleStarGiftsNotifications(enabled: Bool) { self.impl.with { impl in impl.toggleStarGiftsNotifications(enabled: enabled) } } public func updateFilter(_ filter: ProfileGiftsContext.Filters) { self.impl.with { impl in impl.updateFilter(filter) } } public func updateSorting(_ sorting: ProfileGiftsContext.Sorting) { self.impl.with { impl in impl.updateSorting(sorting) } } public var currentState: ProfileGiftsContext.State? { var state: ProfileGiftsContext.State? self.impl.syncWith { impl in state = impl._state } return state } } extension ProfileGiftsContext.State.StarGift { init?(apiSavedStarGift: Api.SavedStarGift, peerId: EnginePeer.Id, transaction: Transaction) { switch apiSavedStarGift { case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars): guard let gift = StarGift(apiStarGift: apiGift) else { return nil } self.gift = gift if let fromPeerId = fromId?.peerId { self.fromPeer = transaction.getPeer(fromPeerId).flatMap(EnginePeer.init) } else { self.fromPeer = nil } self._fromPeerId = self.fromPeer?.id self.date = date if let message { switch message { case let .textWithEntities(text, entities): self.text = text self.entities = messageTextEntitiesFromApiEntities(entities) } } else { self.text = nil self.entities = nil } if let savedId { self.reference = .peer(peerId: peerId, id: savedId) } else if let msgId { if let fromPeer = self.fromPeer { self.reference = .message(messageId: EngineMessage.Id(peerId: fromPeer.id, namespace: Namespaces.Message.Cloud, id: msgId)) } else if case .unique = gift { self.reference = .message(messageId: EngineMessage.Id(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), namespace: Namespaces.Message.Cloud, id: msgId)) } else { self.reference = nil } } else { self.reference = nil } self.nameHidden = (flags & (1 << 0)) != 0 self.savedToProfile = (flags & (1 << 5)) == 0 self.convertStars = convertStars self.canUpgrade = (flags & (1 << 10)) != 0 self.canExportDate = canExportDate self.upgradeStars = upgradeStars self.transferStars = transferStars } } } extension StarGift.UniqueGift.Attribute { init?(apiAttribute: Api.StarGiftAttribute) { switch apiAttribute { case let .starGiftAttributeModel(name, document, rarityPermille): guard let file = telegramMediaFileFromApiDocument(document, altDocuments: nil) else { return nil } self = .model(name: name, file: file, rarity: rarityPermille) case let .starGiftAttributePattern(name, document, rarityPermille): guard let file = telegramMediaFileFromApiDocument(document, altDocuments: nil) else { return nil } self = .pattern(name: name, file: file, rarity: rarityPermille) case let .starGiftAttributeBackdrop(name, centerColor, edgeColor, patternColor, textColor, rarityPermille): self = .backdrop(name: name, innerColor: centerColor, outerColor: edgeColor, patternColor: patternColor, textColor: textColor, rarity: rarityPermille) case let .starGiftAttributeOriginalDetails(_, sender, recipient, date, message): var text: String? var entities: [MessageTextEntity]? if case let .textWithEntities(textValue, entitiesValue) = message { text = textValue entities = messageTextEntitiesFromApiEntities(entitiesValue) } self = .originalInfo(senderPeerId: sender?.peerId, recipientPeerId: recipient.peerId, date: date, text: text, entities: entities) } } } func _internal_getUniqueStarGift(account: Account, slug: String) -> Signal { return account.network.request(Api.functions.payments.getUniqueStarGift(slug: slug)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal in if let result = result { switch result { case let .uniqueStarGift(gift, users): return account.postbox.transaction { transaction in let parsedPeers = AccumulatedPeers(users: users) updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) guard case let .unique(uniqueGift) = StarGift(apiStarGift: gift) else { return nil } return uniqueGift } } } else { return .single(nil) } } } public enum StarGiftReference: Equatable, Hashable, Codable { enum CodingKeys: String, CodingKey { case type case messageId case peerId case id } case message(messageId: EngineMessage.Id) case peer(peerId: EnginePeer.Id, id: Int64) public enum DecodingError: Error { case generic } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(Int32.self, forKey: .type) switch type { case 0: self = .message(messageId: try container.decode(EngineMessage.Id.self, forKey: .messageId)) case 1: self = .peer(peerId: try container.decode(EnginePeer.Id.self, forKey: .peerId), id: try container.decode(Int64.self, forKey: .id)) default: throw DecodingError.generic } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case let .message(messageId): try container.encode(0 as Int32, forKey: .type) try container.encode(messageId, forKey: .messageId) case let .peer(peerId, id): try container.encode(1 as Int32, forKey: .type) try container.encode(peerId, forKey: .peerId) try container.encode(id, forKey: .id) } } } extension StarGiftReference { func apiStarGiftReference(transaction: Transaction) -> Api.InputSavedStarGift? { switch self { case let .message(messageId): return .inputSavedStarGiftUser(msgId: messageId.id) case let .peer(peerId, id): guard let inputPeer = transaction.getPeer(peerId).flatMap({ apiInputPeer($0) }) else { return nil } return .inputSavedStarGiftChat(peer: inputPeer, savedId: id) } } } public enum RequestStarGiftWithdrawalError : Equatable { case generic case twoStepAuthMissing case twoStepAuthTooFresh(Int32) case authSessionTooFresh(Int32) case limitExceeded case requestPassword case invalidPassword case serverProvided(text: String) } func _internal_checkStarGiftWithdrawalAvailability(account: Account, reference: StarGiftReference) -> Signal { return account.postbox.transaction { transaction in return reference.apiStarGiftReference(transaction: transaction) } |> castError(RequestStarGiftWithdrawalError.self) |> mapToSignal { starGift in guard let starGift else { return .fail(.generic) } return account.network.request(Api.functions.payments.getStarGiftWithdrawalUrl(stargift: starGift, password: .inputCheckPasswordEmpty)) |> mapError { error -> RequestStarGiftWithdrawalError in if error.errorDescription == "PASSWORD_HASH_INVALID" { return .requestPassword } else if error.errorDescription == "PASSWORD_MISSING" { return .twoStepAuthMissing } else if error.errorDescription.hasPrefix("PASSWORD_TOO_FRESH_") { let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "PASSWORD_TOO_FRESH_".count)...]) if let value = Int32(timeout) { return .twoStepAuthTooFresh(value) } } else if error.errorDescription.hasPrefix("SESSION_TOO_FRESH_") { let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "SESSION_TOO_FRESH_".count)...]) if let value = Int32(timeout) { return .authSessionTooFresh(value) } } return .generic } |> ignoreValues } } func _internal_requestStarGiftWithdrawalUrl(account: Account, reference: StarGiftReference, password: String) -> Signal { guard !password.isEmpty else { return .fail(.invalidPassword) } return account.postbox.transaction { transaction -> Signal in guard let starGift = reference.apiStarGiftReference(transaction: transaction) else { return .fail(.generic) } let checkPassword = _internal_twoStepAuthData(account.network) |> mapError { error -> RequestStarGiftWithdrawalError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else { return .generic } } |> mapToSignal { authData -> Signal in if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData { guard let kdfResult = passwordKDF(encryptionProvider: account.network.encryptionProvider, password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else { return .fail(.generic) } return .single(.inputCheckPasswordSRP(srpId: kdfResult.id, A: Buffer(data: kdfResult.A), M1: Buffer(data: kdfResult.M1))) } else { return .fail(.twoStepAuthMissing) } } return checkPassword |> mapToSignal { password -> Signal in return account.network.request(Api.functions.payments.getStarGiftWithdrawalUrl(stargift: starGift, password: password), automaticFloodWait: false) |> mapError { error -> RequestStarGiftWithdrawalError in if error.errorCode == 406 { return .serverProvided(text: error.errorDescription) } else if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else if error.errorDescription == "PASSWORD_HASH_INVALID" { return .invalidPassword } else if error.errorDescription == "PASSWORD_MISSING" { return .twoStepAuthMissing } else if error.errorDescription.hasPrefix("PASSWORD_TOO_FRESH_") { let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "PASSWORD_TOO_FRESH_".count)...]) if let value = Int32(timeout) { return .twoStepAuthTooFresh(value) } } else if error.errorDescription.hasPrefix("SESSION_TOO_FRESH_") { let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "SESSION_TOO_FRESH_".count)...]) if let value = Int32(timeout) { return .authSessionTooFresh(value) } } return .generic } |> map { result -> String in switch result { case let .starGiftWithdrawalUrl(url): return url } } } } |> mapError { _ -> RequestStarGiftWithdrawalError in } |> switchToLatest } func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer.Id, enabled: Bool) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } |> mapToSignal { inputPeer in guard let inputPeer else { return .complete() } var flags: Int32 = 0 if enabled { flags |= (1 << 0) } return account.network.request(Api.functions.payments.toggleChatStarGiftNotifications(flags: flags, peer: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> ignoreValues } } public extension StarGift.UniqueGift { var itemFile: TelegramMediaFile? { for attribute in self.attributes { if case let .model(_, file, _) = attribute { return file } } return nil } }