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 title 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 case resale case minResaleStars } public let remains: Int32 public let total: Int32 public let resale: Int64 public let minResaleStars: Int64? public init(remains: Int32, total: Int32, resale: Int64, minResaleStars: Int64?) { self.remains = remains self.total = total self.resale = resale self.minResaleStars = minResaleStars } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.remains = try container.decode(Int32.self, forKey: .remains) self.total = try container.decode(Int32.self, forKey: .total) self.resale = (try? container.decodeIfPresent(Int64.self, forKey: .resale)) ?? 0 self.minResaleStars = try? container.decodeIfPresent(Int64.self, forKey: .minResaleStars) } public init(decoder: PostboxDecoder) { self.remains = decoder.decodeInt32ForKey(CodingKeys.remains.rawValue, orElse: 0) self.total = decoder.decodeInt32ForKey(CodingKeys.total.rawValue, orElse: 0) self.resale = decoder.decodeInt64ForKey(CodingKeys.resale.rawValue, orElse: 0) self.minResaleStars = decoder.decodeInt64ForKey(CodingKeys.minResaleStars.rawValue, orElse: 0) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.remains, forKey: .remains) try container.encode(self.total, forKey: .total) try container.encode(self.resale, forKey: .resale) try container.encodeIfPresent(self.minResaleStars, forKey: .minResaleStars) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.remains, forKey: CodingKeys.remains.rawValue) encoder.encodeInt32(self.total, forKey: CodingKeys.total.rawValue) encoder.encodeInt64(self.resale, forKey: CodingKeys.resale.rawValue) if let minResaleStars = self.minResaleStars { encoder.encodeInt64(minResaleStars, forKey: CodingKeys.minResaleStars.rawValue) } else { encoder.encodeNil(forKey: CodingKeys.minResaleStars.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 title: String? 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, title: String?, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?, flags: Flags, upgradeStars: Int64?) { self.id = id self.title = title 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) self.title = try container.decodeIfPresent(String.self, forKey: .title) 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.title = decoder.decodeOptionalStringForKey(CodingKeys.title.rawValue) 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) try container.encodeIfPresent(self.title, forKey: .title) 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) if let title = self.title { encoder.encodeString(title, forKey: CodingKeys.title.rawValue) } else { encoder.encodeNil(forKey: CodingKeys.title.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 case resellStars } public enum Attribute: Equatable, Codable, PostboxCoding { enum CodingKeys: String, CodingKey { case type case name case file case id 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, id: Int32, 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), id: try container.decodeIfPresent(Int32.self, forKey: .id) ?? 0, 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: ""), id: decoder.decodeInt32ForKey(CodingKeys.id.rawValue, orElse: 0), 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, id, innerColor, outerColor, patternColor, textColor, rarity): try container.encode(Int32(2), forKey: .type) try container.encode(name, forKey: .name) try container.encode(id, forKey: .id) 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, id, innerColor, outerColor, patternColor, textColor, rarity): encoder.encodeInt32(2, forKey: CodingKeys.type.rawValue) encoder.encodeString(name, forKey: CodingKeys.name.rawValue) encoder.encodeInt32(id, forKey: CodingKeys.id.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 var peerId: EnginePeer.Id? { if case let .peerId(peerId) = self { return peerId } return nil } } 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 let resellStars: Int64? public init(id: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellStars: Int64?) { self.id = id self.title = title self.number = number self.slug = slug self.owner = owner self.attributes = attributes self.availability = availability self.giftAddress = giftAddress self.resellStars = resellStars } 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) self.resellStars = try container.decodeIfPresent(Int64.self, forKey: .resellStars) } 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) self.resellStars = decoder.decodeOptionalInt64ForKey(CodingKeys.resellStars.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) try container.encodeIfPresent(self.resellStars, forKey: .resellStars) } 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) } if let resellStars = self.resellStars { encoder.encodeInt64(resellStars, forKey: CodingKeys.resellStars.rawValue) } else { encoder.encodeNil(forKey: CodingKeys.resellStars.rawValue) } } public func withResellStars(_ resellStars: Int64?) -> UniqueGift { return UniqueGift( id: self.id, title: self.title, number: self.number, slug: self.slug, owner: self.owner, attributes: self.attributes, availability: self.availability, giftAddress: self.giftAddress, resellStars: resellStars ) } } 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, availabilityResale, convertStars, firstSale, lastSale, upgradeStars, minResaleStars, title): 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, resale: availabilityResale ?? 0, minResaleStars: minResaleStars ) } 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, title: title, 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, reselltars): 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, resellStars: reselltars)) } } } 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 } } func _internal_updateStarGiftsPinnedToTop(account: Account, peerId: EnginePeer.Id, references: [StarGiftReference]) -> Signal { return account.postbox.transaction { transaction in let peer = transaction.getPeer(peerId) let starGifts = references.compactMap { $0.apiStarGiftReference(transaction: transaction) } return (peer, starGifts) } |> mapToSignal { peer, starGifts in guard let inputPeer = peer.flatMap(apiInputPeer) else { return .complete() } return account.network.request(Api.functions.payments.toggleStarGiftsPinnedToTop(peer: inputPeer, stargift: starGifts)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> ignoreValues } } public enum TransferStarGiftError { case generic case disallowedStarGift } public enum BuyStarGiftError { case generic case priceChanged(Int64) case starGiftResellTooEarly(Int32) } public enum UpdateStarGiftPriceError { case generic case starGiftResellTooEarly(Int32) } public enum UpgradeStarGiftError { case generic } func _internal_buyStarGift(account: Account, slug: String, peerId: EnginePeer.Id, price: Int64?) -> Signal { let source: BotPaymentInvoiceSource = .starGiftResale(slug: slug, 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 let .starGiftResellTooEarly(timestamp) = error { return .fail(.starGiftResellTooEarly(timestamp)) } return .fail(.generic) } |> mapToSignal { paymentForm in if let paymentForm { if let paymentPrice = paymentForm.invoice.prices.first?.amount, let price, paymentPrice > price { return .fail(.priceChanged(paymentPrice)) } return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source) |> mapError { _ -> BuyStarGiftError in return .generic } |> ignoreValues } else { return .fail(.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 { error -> TransferStarGiftError in if error.errorDescription == "USER_DISALLOWED_STARGIFTS" { return .disallowedStarGift } 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) } else if case .disallowedStarGift = error { return .fail(.disallowedStarGift) } 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) } } } } } 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, _, canTransferDate, canResaleDate) = 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, pinnedToTop: false, convertStars: nil, canUpgrade: false, canExportDate: canExportDate, upgradeStars: nil, transferStars: transferStars, canTransferDate: canTransferDate, canResaleDate: canResaleDate )) } } } 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 private var filter: ProfileGiftsContext.Filters 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, sorting: ProfileGiftsContext.Sorting, filter: ProfileGiftsContext.Filters ) { self.queue = queue self.account = account self.peerId = peerId self.sorting = sorting self.filter = filter 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 if !isFiltered { self.filteredGifts = [] self.filteredCount = nil } let isUniqueOnlyFilter = self.filter == [.unique, .displayed, .hidden] let dataState = isFiltered ? self.filteredDataState : self.dataState if case let .ready(true, initialNextOffset) = dataState { if !isFiltered || isUniqueOnlyFilter, self.gifts.isEmpty, initialNextOffset == nil, !reload { 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 isUniqueOnlyFilter, case .loading = self.filteredDataState { var gifts = cachedGifts.gifts if isUniqueOnlyFilter { gifts = gifts.filter({ gift in if case .unique = gift.gift { return true } else { return false } }) } self.gifts = gifts self.count = cachedGifts.count self.notificationsEnabled = cachedGifts.notificationsEnabled self.pushState() } else 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: 36)) |> 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, apiGifts, nextOffset, chats, users): 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 }) { if !added && self.gifts[index].pinnedToTop { let pinnedGifts = self.gifts.filter { $0.pinnedToTop && $0.reference != reference } let existingGifts = Set(pinnedGifts.compactMap { $0.reference }) var updatedGifts: [ProfileGiftsContext.State.StarGift] = [] for gift in self.gifts { if let reference = gift.reference, existingGifts.contains(reference) { continue } var gift = gift if gift.reference == reference { gift = gift.withPinnedToTop(false).withSavedToProfile(false) } updatedGifts.append(gift) } updatedGifts.sort { lhs, rhs in lhs.date > rhs.date } updatedGifts.insert(contentsOf: pinnedGifts, at: 0) self.gifts = updatedGifts } else { 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 updateStarGiftPinnedToTop(reference: StarGiftReference, pinnedToTop: Bool) { var pinnedGifts = self.gifts.filter { $0.pinnedToTop } var saveToProfile = false if var gift = self.gifts.first(where: { $0.reference == reference }) { gift = gift.withPinnedToTop(pinnedToTop) if pinnedToTop { if !gift.savedToProfile { gift = gift.withSavedToProfile(true) saveToProfile = true } pinnedGifts.append(gift) } else { pinnedGifts.removeAll(where: { $0.reference == reference }) } } let existingGifts = Set(pinnedGifts.compactMap { $0.reference }) var updatedGifts: [ProfileGiftsContext.State.StarGift] = [] for gift in self.gifts { if let reference = gift.reference, existingGifts.contains(reference) { continue } var gift = gift if gift.reference == reference { gift = gift.withPinnedToTop(pinnedToTop) } updatedGifts.append(gift) } updatedGifts.sort { lhs, rhs in lhs.date > rhs.date } updatedGifts.insert(contentsOf: pinnedGifts, at: 0) self.gifts = updatedGifts var effectiveReferences = pinnedGifts.compactMap { $0.reference } if !self.filteredGifts.isEmpty { var filteredPinnedGifts = self.filteredGifts.filter { $0.pinnedToTop } if var gift = self.filteredGifts.first(where: { $0.reference == reference }) { gift = gift.withPinnedToTop(pinnedToTop) if pinnedToTop { if !gift.savedToProfile { gift = gift.withSavedToProfile(true) } filteredPinnedGifts.append(gift) } else { filteredPinnedGifts.removeAll(where: { $0.reference == reference }) } } let existingFilteredGifts = Set(filteredPinnedGifts.compactMap { $0.reference }) var updatedFilteredGifts: [ProfileGiftsContext.State.StarGift] = [] for gift in self.filteredGifts { if let reference = gift.reference, existingFilteredGifts.contains(reference) { continue } var gift = gift if gift.reference == reference { gift = gift.withPinnedToTop(pinnedToTop) } updatedFilteredGifts.append(gift) } updatedFilteredGifts.sort { lhs, rhs in lhs.date > rhs.date } updatedFilteredGifts.insert(contentsOf: filteredPinnedGifts, at: 0) self.filteredGifts = updatedFilteredGifts effectiveReferences = filteredPinnedGifts.compactMap { $0.reference } } self.pushState() var signal = _internal_updateStarGiftsPinnedToTop(account: self.account, peerId: self.peerId, references: effectiveReferences) if saveToProfile { signal = _internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true) |> then(signal) } self.actionDisposable.set( (signal |> deliverOn(self.queue)).startStrict(completed: { [weak self] in self?.reload() }) ) } public func updatePinnedToTopStarGifts(references: [StarGiftReference]) { let existingGifts = Set(references) var saveSignals: [Signal] = [] let currentPinnedGifts = self.gifts.filter { gift in if let reference = gift.reference { return existingGifts.contains(reference) } else { return false } }.map { gift in if !gift.savedToProfile, let reference = gift.reference { saveSignals.append(_internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true)) } return gift.withPinnedToTop(true).withSavedToProfile(true) } var updatedGifts: [ProfileGiftsContext.State.StarGift] = [] for gift in self.gifts { if let reference = gift.reference, existingGifts.contains(reference) { continue } updatedGifts.append(gift.withPinnedToTop(false)) } updatedGifts.sort { lhs, rhs in lhs.date > rhs.date } var pinnedGifts: [ProfileGiftsContext.State.StarGift] = [] for reference in references { if let gift = currentPinnedGifts.first(where: { $0.reference == reference }) { pinnedGifts.append(gift) } } updatedGifts.insert(contentsOf: pinnedGifts, at: 0) self.gifts = updatedGifts self.pushState() var signal = _internal_updateStarGiftsPinnedToTop(account: self.account, peerId: self.peerId, references: pinnedGifts.compactMap { $0.reference }) if !saveSignals.isEmpty { signal = combineLatest(saveSignals) |> ignoreValues |> then(signal) } self.actionDisposable.set( (signal |> deliverOn(self.queue)).startStrict(completed: { [weak self] in self?.reload() }) ) } 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) -> Signal { 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() return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId) } func buyStarGift(slug: String, peerId: EnginePeer.Id, price: Int64?) -> Signal { var listingPrice: Int64? if let gift = self.gifts.first(where: { gift in if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug { return true } return false }), case let .unique(uniqueGift) = gift.gift { listingPrice = uniqueGift.resellStars } if listingPrice == nil { if let gift = self.filteredGifts.first(where: { gift in if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug { return true } return false }), case let .unique(uniqueGift) = gift.gift { listingPrice = uniqueGift.resellStars } } return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId, price: price ?? listingPrice) |> afterCompleted { [weak self] in guard let self else { return } self.queue.async { if let count = self.count { self.count = max(0, count - 1) } self.gifts.removeAll(where: { gift in if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug { return true } return false }) self.filteredGifts.removeAll(where: { gift in if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug { return true } return false }) self.pushState() } } } func removeStarGift(gift: TelegramCore.StarGift) { self.gifts.removeAll(where: { $0.gift == gift }) self.filteredGifts.removeAll(where: { $0.gift == gift }) 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 ) |> deliverOn(self.queue)).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 updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?, id: Int64?) -> Signal { return Signal { [weak self] subscriber in guard let self else { return EmptyDisposable } let signal = _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price) let disposable = MetaDisposable() disposable.set( (signal |> deliverOn(self.queue)).startStrict(error: { error in subscriber.putError(error) }, completed: { if let index = self.gifts.firstIndex(where: { gift in if gift.reference == reference { return true } switch gift.gift { case .generic(let gift): if gift.id == id { return true } case .unique(let uniqueGift): if uniqueGift.id == id { return true } } return false }) { if case let .unique(uniqueGift) = self.gifts[index].gift { let updatedUniqueGift = uniqueGift.withResellStars(price) let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift)) self.gifts[index] = updatedGift } } if let index = self.filteredGifts.firstIndex(where: { gift in if gift.reference == reference { return true } switch gift.gift { case .generic(let gift): if gift.id == id { return true } case .unique(let uniqueGift): if uniqueGift.id == id { return true } } return false }) { if case let .unique(uniqueGift) = self.filteredGifts[index].gift { let updatedUniqueGift = uniqueGift.withResellStars(price) let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift)) self.filteredGifts[index] = updatedGift } } self.pushState() 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) { guard self.filter != filter else { return } self.filter = filter self.filteredDataState = .ready(canLoadMore: true, nextOffset: nil) self.pushState() self.loadMore() } func updateSorting(_ sorting: ProfileGiftsContext.Sorting) { guard self.sorting != sorting else { return } 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 pinnedToTop case convertStars case canUpgrade case canExportDate case upgradeStars case transferStars case giftAddress case canTransferDate case canResaleDate } 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 pinnedToTop: Bool public let convertStars: Int64? public let canUpgrade: Bool public let canExportDate: Int32? public let upgradeStars: Int64? public let transferStars: Int64? public let canTransferDate: Int32? public let canResaleDate: Int32? 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, pinnedToTop: Bool, convertStars: Int64?, canUpgrade: Bool, canExportDate: Int32?, upgradeStars: Int64?, transferStars: Int64?, canTransferDate: Int32?, canResaleDate: Int32? ) { 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.pinnedToTop = pinnedToTop self.convertStars = convertStars self.canUpgrade = canUpgrade self.canExportDate = canExportDate self.upgradeStars = upgradeStars self.transferStars = transferStars self.canTransferDate = canTransferDate self.canResaleDate = canResaleDate } 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.pinnedToTop = try container.decodeIfPresent(Bool.self, forKey: .pinnedToTop) ?? false 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) self.canTransferDate = try container.decodeIfPresent(Int32.self, forKey: .canTransferDate) self.canResaleDate = try container.decodeIfPresent(Int32.self, forKey: .canResaleDate) } 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.encode(self.pinnedToTop, forKey: .pinnedToTop) 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) try container.encodeIfPresent(self.canTransferDate, forKey: .canTransferDate) try container.encodeIfPresent(self.canResaleDate, forKey: .canResaleDate) } public func withGift(_ gift: TelegramCore.StarGift) -> StarGift { return StarGift( gift: gift, reference: self.reference, fromPeer: self.fromPeer, date: self.date, text: self.text, entities: self.entities, nameHidden: self.nameHidden, savedToProfile: self.savedToProfile, pinnedToTop: self.pinnedToTop, convertStars: self.convertStars, canUpgrade: self.canUpgrade, canExportDate: self.canExportDate, upgradeStars: self.upgradeStars, transferStars: self.transferStars, canTransferDate: self.canTransferDate, canResaleDate: self.canResaleDate ) } 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, pinnedToTop: self.pinnedToTop, convertStars: self.convertStars, canUpgrade: self.canUpgrade, canExportDate: self.canExportDate, upgradeStars: self.upgradeStars, transferStars: self.transferStars, canTransferDate: self.canTransferDate, canResaleDate: self.canResaleDate ) } public func withPinnedToTop(_ pinnedToTop: 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: self.savedToProfile, pinnedToTop: pinnedToTop, convertStars: self.convertStars, canUpgrade: self.canUpgrade, canExportDate: self.canExportDate, upgradeStars: self.upgradeStars, transferStars: self.transferStars, canTransferDate: self.canTransferDate, canResaleDate: self.canResaleDate ) } 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: self.savedToProfile, pinnedToTop: self.pinnedToTop, convertStars: self.convertStars, canUpgrade: self.canUpgrade, canExportDate: self.canExportDate, upgradeStars: self.upgradeStars, transferStars: self.transferStars, canTransferDate: self.canTransferDate, canResaleDate: self.canResaleDate ) } } 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, sorting: ProfileGiftsContext.Sorting = .date, filter: ProfileGiftsContext.Filters = .All ) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { return ProfileGiftsContextImpl(queue: queue, account: account, peerId: peerId, sorting: sorting, filter: filter) }) } 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 updateStarGiftPinnedToTop(reference: StarGiftReference, pinnedToTop: Bool) { self.impl.with { impl in impl.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop) } } public func updatePinnedToTopStarGifts(references: [StarGiftReference]) { self.impl.with { impl in impl.updatePinnedToTopStarGifts(references: references) } } public func convertStarGift(reference: StarGiftReference) { self.impl.with { impl in impl.convertStarGift(reference: reference) } } public func buyStarGift(slug: String, peerId: EnginePeer.Id, price: Int64? = nil) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.buyStarGift(slug: slug, peerId: peerId, price: price).start(error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() })) } return disposable } } public func removeStarGift(gift: TelegramCore.StarGift) { self.impl.with { impl in impl.removeStarGift(gift: gift) } } public func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId).start(error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() })) } return disposable } } 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 updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?, id: Int64? = nil) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.updateStarGiftResellPrice(reference: reference, price: price, id: id).start(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, canTransferAt, canResaleAt): 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.pinnedToTop = (flags & (1 << 12)) != 0 self.convertStars = convertStars self.canUpgrade = (flags & (1 << 10)) != 0 self.canExportDate = canExportDate self.upgradeStars = upgradeStars self.transferStars = transferStars self.canTransferDate = canTransferAt self.canResaleDate = canResaleAt } } } 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, id, centerColor, edgeColor, patternColor, textColor, rarityPermille): self = .backdrop(name: name, id: id, 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 slug } case message(messageId: EngineMessage.Id) case peer(peerId: EnginePeer.Id, id: Int64) case slug(slug: String) 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)) case 2: self = .slug(slug: try container.decode(String.self, forKey: .slug)) 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) case let .slug(slug): try container.encode(2 as Int32, forKey: .type) try container.encode(slug, forKey: .slug) } } } 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) case let .slug(slug): return .inputSavedStarGiftSlug(slug: slug) } } } 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 } } func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal { return account.postbox.transaction { transaction in return reference.apiStarGiftReference(transaction: transaction) } |> castError(UpdateStarGiftPriceError.self) |> mapToSignal { starGift in guard let starGift else { return .complete() } return account.network.request(Api.functions.payments.updateStarGiftPrice(stargift: starGift, resellStars: price ?? 0)) |> mapError { error -> UpdateStarGiftPriceError in if error.errorDescription.hasPrefix("STARGIFT_RESELL_TOO_EARLY_") { let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "STARGIFT_RESELL_TOO_EARLY_".count)...]) if let value = Int32(timeout) { return .starGiftResellTooEarly(value) } } return .generic } |> mapToSignal { updates -> Signal in account.stateManager.addUpdates(updates) return .complete() } |> ignoreValues } } public extension StarGift.UniqueGift { var itemFile: TelegramMediaFile? { for attribute in self.attributes { if case let .model(_, file, _) = attribute { return file } } return nil } } private final class ResaleGiftsContextImpl { private let queue: Queue private let account: Account private let giftId: Int64 private let disposable = MetaDisposable() private var sorting: ResaleGiftsContext.Sorting = .value private var filterAttributes: [ResaleGiftsContext.Attribute] = [] private var gifts: [StarGift] = [] private var attributes: [StarGift.UniqueGift.Attribute] = [] private var attributeCount: [ResaleGiftsContext.Attribute: Int32] = [:] private var attributesHash: Int64? private var count: Int32? private var dataState: ResaleGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil) var _state: ResaleGiftsContext.State? private let stateValue = Promise() var state: Signal { return self.stateValue.get() } init( queue: Queue, account: Account, giftId: Int64 ) { self.queue = queue self.account = account self.giftId = giftId self.loadMore() } deinit { self.disposable.dispose() } func reload() { self.gifts = [] self.dataState = .ready(canLoadMore: true, nextOffset: nil) self.loadMore(reload: true) } func loadMore(reload: Bool = false) { let giftId = self.giftId let accountPeerId = self.account.peerId let network = self.account.network let postbox = self.account.postbox let sorting = self.sorting let filterAttributes = self.filterAttributes let currentAttributesHash = self.attributesHash let dataState = self.dataState if case let .ready(true, initialNextOffset) = dataState { self.dataState = .loading if !reload { self.pushState() } var flags: Int32 = 0 switch sorting { case .date: break case .value: flags |= (1 << 1) case .number: flags |= (1 << 2) } var apiAttributes: [Api.StarGiftAttributeId]? if !filterAttributes.isEmpty { flags |= (1 << 3) apiAttributes = filterAttributes.map { switch $0 { case let .model(id): return .starGiftAttributeIdModel(documentId: id) case let .pattern(id): return .starGiftAttributeIdPattern(documentId: id) case let .backdrop(id): return .starGiftAttributeIdBackdrop(backdropId: id) } } } let attributesHash = currentAttributesHash ?? 0 flags |= (1 << 0) let signal = network.request(Api.functions.payments.getResaleStarGifts(flags: flags, attributesHash: attributesHash, giftId: giftId, attributes: apiAttributes, offset: initialNextOffset ?? "", limit: 36)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal<([StarGift], [StarGift.UniqueGift.Attribute]?, [ResaleGiftsContext.Attribute: Int32]?, Int64?, Int32, String?), NoError> in guard let result else { return .single(([], nil, nil, nil, 0, nil)) } return postbox.transaction { transaction -> ([StarGift], [StarGift.UniqueGift.Attribute]?, [ResaleGiftsContext.Attribute: Int32]?, Int64?, Int32, String?) in switch result { case let .resaleStarGifts(_, count, gifts, nextOffset, attributes, attributesHash, chats, counters, users): let _ = attributesHash var resultAttributes: [StarGift.UniqueGift.Attribute]? if let attributes { resultAttributes = attributes.compactMap { StarGift.UniqueGift.Attribute(apiAttribute: $0) } } var attributeCount: [ResaleGiftsContext.Attribute: Int32]? if let counters { var attributeCountValue: [ResaleGiftsContext.Attribute: Int32] = [:] for counter in counters { switch counter { case let .starGiftAttributeCounter(attribute, count): switch attribute { case let .starGiftAttributeIdModel(documentId): attributeCountValue[.model(documentId)] = count case let .starGiftAttributeIdPattern(documentId): attributeCountValue[.pattern(documentId)] = count case let .starGiftAttributeIdBackdrop(backdropId): attributeCountValue[.backdrop(backdropId)] = count } } } attributeCount = attributeCountValue } let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) var mappedGifts: [StarGift] = [] for gift in gifts { if let mappedGift = StarGift(apiStarGift: gift), case let .unique(uniqueGift) = mappedGift, let resellStars = uniqueGift.resellStars, resellStars > 0 { mappedGifts.append(mappedGift) } } return (mappedGifts, resultAttributes, attributeCount, attributesHash, count, nextOffset) } } } self.disposable.set((signal |> deliverOn(self.queue)).start(next: { [weak self] (gifts, attributes, attributeCount, attributesHash, count, nextOffset) in guard let self else { return } if initialNextOffset == nil || reload { self.gifts = gifts } else { self.gifts.append(contentsOf: gifts) } let updatedCount = max(Int32(self.gifts.count), count) self.count = updatedCount if let attributes, let attributeCount, let attributesHash { self.attributes = attributes self.attributeCount = attributeCount self.attributesHash = attributesHash } self.dataState = .ready(canLoadMore: count != 0 && updatedCount > self.gifts.count && nextOffset != nil, nextOffset: nextOffset) self.pushState() })) } } func updateFilterAttributes(_ filterAttributes: [ResaleGiftsContext.Attribute]) { guard self.filterAttributes != filterAttributes else { return } self.filterAttributes = filterAttributes self.dataState = .ready(canLoadMore: true, nextOffset: nil) self.pushState() self.loadMore() } func removeStarGift(gift: TelegramCore.StarGift) { self.gifts.removeAll(where: { $0 == gift }) self.pushState() } func updateSorting(_ sorting: ResaleGiftsContext.Sorting) { guard self.sorting != sorting else { return } self.sorting = sorting self.dataState = .ready(canLoadMore: true, nextOffset: nil) self.pushState() self.loadMore() } func buyStarGift(slug: String, peerId: EnginePeer.Id, price: Int64?) -> Signal { var listingPrice: Int64? if let gift = self.gifts.first(where: { gift in if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug { return true } return false }), case let .unique(uniqueGift) = gift { listingPrice = uniqueGift.resellStars } return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId, price: price ?? listingPrice) |> afterCompleted { [weak self] in guard let self else { return } self.queue.async { if let count = self.count { self.count = max(0, count - 1) } self.gifts.removeAll(where: { gift in if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug { return true } return false }) self.pushState() } } } func updateStarGiftResellPrice(slug: String, price: Int64?) -> Signal { return Signal { [weak self] subscriber in guard let self else { return EmptyDisposable } let disposable = MetaDisposable() disposable.set( (_internal_updateStarGiftResalePrice( account: self.account, reference: .slug(slug: slug), price: price ) |> deliverOn(self.queue)).startStrict(error: { error in subscriber.putError(error) }, completed: { if let index = self.gifts.firstIndex(where: { gift in if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug { return true } return false }) { if let price { if case let .unique(uniqueGift) = self.gifts[index] { self.gifts[index] = .unique(uniqueGift.withResellStars(price)) } } else { self.gifts.remove(at: index) } } self.pushState() subscriber.putCompletion() }) ) return disposable } } private func pushState() { let state = ResaleGiftsContext.State( sorting: self.sorting, filterAttributes: self.filterAttributes, gifts: self.gifts, attributes: self.attributes, attributeCount: self.attributeCount, count: self.count, dataState: self.dataState ) self._state = state self.stateValue.set(.single(state)) } } public final class ResaleGiftsContext { public enum Sorting: Equatable { case date case value case number } public enum Attribute: Equatable, Hashable { case model(Int64) case pattern(Int64) case backdrop(Int32) } public struct State: Equatable { public enum DataState: Equatable { case loading case ready(canLoadMore: Bool, nextOffset: String?) } public var sorting: Sorting public var filterAttributes: [Attribute] public var gifts: [StarGift] public var attributes: [StarGift.UniqueGift.Attribute] public var attributeCount: [Attribute: Int32] public var count: Int32? public var dataState: ResaleGiftsContext.State.DataState } 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, giftId: Int64 ) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { return ResaleGiftsContextImpl(queue: queue, account: account, giftId: giftId) }) } public func loadMore() { self.impl.with { impl in impl.loadMore() } } public func updateSorting(_ sorting: ResaleGiftsContext.Sorting) { self.impl.with { impl in impl.updateSorting(sorting) } } public func updateFilterAttributes(_ attributes: [ResaleGiftsContext.Attribute]) { self.impl.with { impl in impl.updateFilterAttributes(attributes) } } public func buyStarGift(slug: String, peerId: EnginePeer.Id, price: Int64? = nil) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.buyStarGift(slug: slug, peerId: peerId, price: price).start(error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() })) } return disposable } } public func updateStarGiftResellPrice(slug: String, price: Int64?) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in disposable.set(impl.updateStarGiftResellPrice(slug: slug, price: price).start(error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() })) } return disposable } } public func removeStarGift(gift: TelegramCore.StarGift) { self.impl.with { impl in impl.removeStarGift(gift: gift) } } public var currentState: ResaleGiftsContext.State? { var state: ResaleGiftsContext.State? self.impl.syncWith { impl in state = impl._state } return state } }