import Foundation import Postbox import SwiftSignalKit import TelegramApi private class AdMessagesHistoryContextImpl { final class CachedMessage: Equatable, Codable { enum CodingKeys: String, CodingKey { case opaqueId case messageType case displayAvatar case text case textEntities case media case target case messageId case startParam case sponsorInfo case additionalInfo } enum MessageType: Int32, Codable { case sponsored = 0 case recommended = 1 } enum Target: Equatable, Codable { enum DecodingError: Error { case generic } enum CodingKeys: String, CodingKey { case peer case invite case webPage } struct Invite: Equatable, Codable { var title: String var joinHash: String } struct WebPage: Equatable, Codable { var title: String var url: String } case peer(PeerId) case invite(Invite) case webPage(WebPage) init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) if let peer = try container.decodeIfPresent(Int64.self, forKey: .peer) { self = .peer(PeerId(peer)) } else if let invite = try container.decodeIfPresent(Invite.self, forKey: .invite) { self = .invite(invite) } else if let webPage = try container.decodeIfPresent(WebPage.self, forKey: .webPage) { self = .webPage(webPage) } else { throw DecodingError.generic } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case let .peer(peerId): try container.encode(peerId.toInt64(), forKey: .peer) case let .invite(invite): try container.encode(invite, forKey: .invite) case let .webPage(webPage): try container.encode(webPage, forKey: .webPage) } } } public let opaqueId: Data public let messageType: MessageType public let displayAvatar: Bool public let text: String public let textEntities: [MessageTextEntity] public let media: [Media] public let target: Target public let messageId: MessageId? public let startParam: String? public let sponsorInfo: String? public let additionalInfo: String? public init( opaqueId: Data, messageType: MessageType, displayAvatar: Bool, text: String, textEntities: [MessageTextEntity], media: [Media], target: Target, messageId: MessageId?, startParam: String?, sponsorInfo: String?, additionalInfo: String? ) { self.opaqueId = opaqueId self.messageType = messageType self.displayAvatar = displayAvatar self.text = text self.textEntities = textEntities self.media = media self.target = target self.messageId = messageId self.startParam = startParam self.sponsorInfo = sponsorInfo self.additionalInfo = additionalInfo } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.opaqueId = try container.decode(Data.self, forKey: .opaqueId) if let messageType = try container.decodeIfPresent(Int32.self, forKey: .messageType) { self.messageType = MessageType(rawValue: messageType) ?? .sponsored } else { self.messageType = .sponsored } self.displayAvatar = try container.decodeIfPresent(Bool.self, forKey: .displayAvatar) ?? false self.text = try container.decode(String.self, forKey: .text) self.textEntities = try container.decode([MessageTextEntity].self, forKey: .textEntities) let mediaData = try container.decode([Data].self, forKey: .media) self.media = mediaData.compactMap { data -> Media? in return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media } self.target = try container.decode(Target.self, forKey: .target) self.messageId = try container.decodeIfPresent(MessageId.self, forKey: .messageId) self.startParam = try container.decodeIfPresent(String.self, forKey: .startParam) self.sponsorInfo = try container.decodeIfPresent(String.self, forKey: .sponsorInfo) self.additionalInfo = try container.decodeIfPresent(String.self, forKey: .additionalInfo) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.opaqueId, forKey: .opaqueId) try container.encode(self.messageType.rawValue, forKey: .messageType) try container.encode(self.displayAvatar, forKey: .displayAvatar) try container.encode(self.text, forKey: .text) try container.encode(self.textEntities, forKey: .textEntities) let mediaData = self.media.map { media -> Data in let encoder = PostboxEncoder() encoder.encodeRootObject(media) return encoder.makeData() } try container.encode(mediaData, forKey: .media) try container.encode(self.target, forKey: .target) try container.encodeIfPresent(self.messageId, forKey: .messageId) try container.encodeIfPresent(self.startParam, forKey: .startParam) try container.encodeIfPresent(self.sponsorInfo, forKey: .sponsorInfo) try container.encodeIfPresent(self.additionalInfo, forKey: .additionalInfo) } public static func ==(lhs: CachedMessage, rhs: CachedMessage) -> Bool { if lhs.opaqueId != rhs.opaqueId { return false } if lhs.messageType != rhs.messageType { return false } if lhs.text != rhs.text { return false } if lhs.textEntities != rhs.textEntities { return false } if lhs.media.count != rhs.media.count { return false } for i in 0 ..< lhs.media.count { if !lhs.media[i].isEqual(to: rhs.media[i]) { return false } } if lhs.target != rhs.target { return false } if lhs.messageId != rhs.messageId { return false } if lhs.startParam != rhs.startParam { return false } if lhs.sponsorInfo != rhs.sponsorInfo { return false } if lhs.additionalInfo != rhs.additionalInfo { return false } return true } func toMessage(peerId: PeerId, transaction: Transaction) -> Message? { var attributes: [MessageAttribute] = [] let target: AdMessageAttribute.MessageTarget switch self.target { case let .peer(peerId): target = .peer(id: peerId, message: self.messageId, startParam: self.startParam) case let .invite(invite): target = .join(title: invite.title, joinHash: invite.joinHash) case let .webPage(webPage): target = .webPage(title: webPage.title, url: webPage.url) } let mappedMessageType: AdMessageAttribute.MessageType switch self.messageType { case .sponsored: mappedMessageType = .sponsored case .recommended: mappedMessageType = .recommended } attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, messageType: mappedMessageType, displayAvatar: self.displayAvatar, target: target, sponsorInfo: self.sponsorInfo, additionalInfo: self.additionalInfo)) if !self.textEntities.isEmpty { let attribute = TextEntitiesMessageAttribute(entities: self.textEntities) attributes.append(attribute) } var messagePeers = SimpleDictionary() if let peer = transaction.getPeer(peerId) { messagePeers[peer.id] = peer } let author: Peer switch self.target { case let .peer(peerId): if let peer = transaction.getPeer(peerId) { author = peer } else { return nil } case let .invite(invite): author = TelegramChannel( id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, title: invite.title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .left, info: .broadcast(TelegramChannelBroadcastInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [] ) case let .webPage(webPage): author = TelegramChannel( id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, title: webPage.title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .left, info: .broadcast(TelegramChannelBroadcastInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: []) } messagePeers[author.id] = author let messageHash = (self.text.hashValue &+ 31 &* peerId.hashValue) &* 31 &+ author.id.hashValue let messageStableVersion = UInt32(bitPattern: Int32(truncatingIfNeeded: messageHash)) return Message( stableId: 0, stableVersion: messageStableVersion, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: Int32.max - 1, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: self.text, attributes: attributes, media: self.media, peers: messagePeers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:] ) } } private let queue: Queue private let account: Account private let peerId: PeerId private let maskAsSeenDisposables = DisposableDict() struct CachedState: Codable, PostboxCoding { enum CodingKeys: String, CodingKey { case timestamp case interPostInterval case messages } var timestamp: Int32 var interPostInterval: Int32? var messages: [CachedMessage] init(timestamp: Int32, interPostInterval: Int32?, messages: [CachedMessage]) { self.timestamp = timestamp self.interPostInterval = interPostInterval self.messages = messages } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.timestamp = try container.decode(Int32.self, forKey: .timestamp) self.interPostInterval = try container.decodeIfPresent(Int32.self, forKey: .interPostInterval) self.messages = try container.decode([CachedMessage].self, forKey: .messages) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.timestamp, forKey: .timestamp) try container.encodeIfPresent(self.interPostInterval, forKey: .interPostInterval) try container.encode(self.messages, forKey: .messages) } init(decoder: PostboxDecoder) { self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0) self.interPostInterval = decoder.decodeOptionalInt32ForKey("interPostInterval") if let messagesData = decoder.decodeOptionalDataArrayForKey("messages") { self.messages = messagesData.compactMap { data -> CachedMessage? in return try? AdaptedPostboxDecoder().decode(CachedMessage.self, from: data) } } else { self.messages = [] } } func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.timestamp, forKey: "timestamp") if let interPostInterval = self.interPostInterval { encoder.encodeInt32(interPostInterval, forKey: "interPostInterval") } else { encoder.encodeNil(forKey: "interPostInterval") } encoder.encodeDataArray(self.messages.compactMap { message -> Data? in return try? AdaptedPostboxEncoder().encode(message) }, forKey: "messages") } public static func getCached(postbox: Postbox, peerId: PeerId) -> Signal { return postbox.transaction { transaction -> CachedState? in let key = ValueBoxKey(length: 8) key.setInt64(0, value: peerId.toInt64()) if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key))?.get(CachedState.self) { return entry } else { return nil } } } public static func setCached(transaction: Transaction, peerId: PeerId, state: CachedState?) { let key = ValueBoxKey(length: 8) key.setInt64(0, value: peerId.toInt64()) let id = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key) if let state = state, let entry = CodableEntry(state) { transaction.putItemCacheEntry(id: id, entry: entry) } else { transaction.removeItemCacheEntry(id: id) } } } struct State: Equatable { var interPostInterval: Int32? var messages: [Message] static func ==(lhs: State, rhs: State) -> Bool { if lhs.interPostInterval != rhs.interPostInterval { return false } if lhs.messages.count != rhs.messages.count { return false } for i in 0 ..< lhs.messages.count { if lhs.messages[i].id != rhs.messages[i].id { return false } if lhs.messages[i].stableId != rhs.messages[i].stableId { return false } } return true } } let state = Promise() private var stateValue: State? { didSet { if let stateValue = self.stateValue, stateValue != oldValue { self.state.set(.single(stateValue)) } } } private let disposable = MetaDisposable() init(queue: Queue, account: Account, peerId: PeerId) { self.queue = queue self.account = account self.peerId = peerId self.stateValue = State(interPostInterval: nil, messages: []) self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId) |> mapToSignal { cachedState -> Signal in if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 { return account.postbox.transaction { transaction -> State in return State(interPostInterval: cachedState.interPostInterval, messages: cachedState.messages.compactMap { message -> Message? in return message.toMessage(peerId: peerId, transaction: transaction) }) } } else { return .single(State(interPostInterval: nil, messages: [])) } }) let signal: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputChannel? in return transaction.getPeer(peerId).flatMap(apiInputChannel) } |> mapToSignal { inputChannel -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in guard let inputChannel = inputChannel else { return .single((nil, [])) } return account.network.request(Api.functions.channels.getSponsoredMessages(channel: inputChannel)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in guard let result = result else { return .single((nil, [])) } return account.postbox.transaction { transaction -> (interPostInterval: Int32?, messages: [Message]) in switch result { case let .sponsoredMessages(_, postsBetween, messages, chats, users): var peers: [Peer] = [] var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { peers.append(groupOrChannel) } } for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) peerPresences[telegramUser.id] = user } updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in return updated }) updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) var parsedMessages: [CachedMessage] = [] for message in messages { switch message { case let .sponsoredMessage(flags, randomId, fromId, chatInvite, chatInviteHash, channelPost, startParam, webPage, message, entities, sponsorInfo, additionalInfo): var parsedEntities: [MessageTextEntity] = [] if let entities = entities { parsedEntities = messageTextEntitiesFromApiEntities(entities) } let isRecommended = (flags & (1 << 5)) != 0 let displayAvatar = (flags & (1 << 6)) != 0 var target: CachedMessage.Target? if let fromId = fromId { target = .peer(fromId.peerId) } else if let webPage = webPage { if case let .sponsoredWebPage(_, url, siteName, photo) = webPage { let _ = photo target = .webPage(CachedMessage.Target.WebPage(title: siteName, url: url)) } } else if let chatInvite = chatInvite, let chatInviteHash = chatInviteHash { switch chatInvite { case let .chatInvite(flags, title, _, photo, participantsCount, participants): let photo = telegramMediaImageFromApiPhoto(photo).flatMap({ smallestImageRepresentation($0.representations) }) let flags: ExternalJoiningChatState.Invite.Flags = .init(isChannel: (flags & (1 << 0)) != 0, isBroadcast: (flags & (1 << 1)) != 0, isPublic: (flags & (1 << 2)) != 0, isMegagroup: (flags & (1 << 3)) != 0, requestNeeded: (flags & (1 << 6)) != 0) let _ = photo let _ = flags let _ = participantsCount let _ = participants target = .invite(CachedMessage.Target.Invite( title: title, joinHash: chatInviteHash )) case let .chatInvitePeek(chat, _): if let peer = parseTelegramGroupOrChannel(chat: chat) { target = .invite(CachedMessage.Target.Invite( title: peer.debugDisplayTitle, joinHash: chatInviteHash )) } case let .chatInviteAlready(chat): if let peer = parseTelegramGroupOrChannel(chat: chat) { target = .invite(CachedMessage.Target.Invite( title: peer.debugDisplayTitle, joinHash: chatInviteHash )) } } } var messageId: MessageId? if let fromId = fromId, let channelPost = channelPost { messageId = MessageId(peerId: fromId.peerId, namespace: Namespaces.Message.Cloud, id: channelPost) } if let target = target { parsedMessages.append(CachedMessage( opaqueId: randomId.makeData(), messageType: isRecommended ? .recommended : .sponsored, displayAvatar: displayAvatar, text: message, textEntities: parsedEntities, media: [], target: target, messageId: messageId, startParam: startParam, sponsorInfo: sponsorInfo, additionalInfo: additionalInfo )) } } } CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages)) return (postsBetween, parsedMessages.compactMap { message -> Message? in return message.toMessage(peerId: peerId, transaction: transaction) }) case .sponsoredMessagesEmpty: return (nil, []) } } } } self.disposable.set((signal |> deliverOn(self.queue)).start(next: { [weak self] interPostInterval, messages in guard let strongSelf = self else { return } strongSelf.stateValue = State(interPostInterval: interPostInterval, messages: messages) })) } deinit { self.disposable.dispose() self.maskAsSeenDisposables.dispose() } func markAsSeen(opaqueId: Data) { let signal: Signal = account.postbox.transaction { transaction -> Api.InputChannel? in return transaction.getPeer(self.peerId).flatMap(apiInputChannel) } |> mapToSignal { inputChannel -> Signal in guard let inputChannel = inputChannel else { return .complete() } return self.account.network.request(Api.functions.channels.viewSponsoredMessage(channel: inputChannel, randomId: Buffer(data: opaqueId))) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> ignoreValues } self.maskAsSeenDisposables.set(signal.start(), forKey: opaqueId) } func markAction(opaqueId: Data) { let account = self.account let signal: Signal = account.postbox.transaction { transaction -> Api.InputChannel? in return transaction.getPeer(self.peerId).flatMap(apiInputChannel) } |> mapToSignal { inputChannel -> Signal in guard let inputChannel = inputChannel else { return .complete() } return account.network.request(Api.functions.channels.clickSponsoredMessage(channel: inputChannel, randomId: Buffer(data: opaqueId))) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> ignoreValues } let _ = signal.start() } } public class AdMessagesHistoryContext { private let queue = Queue() private let impl: QueueLocalObject public var state: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in let stateDisposable = impl.state.get().start(next: { state in subscriber.putNext((state.interPostInterval, state.messages)) }) disposable.set(stateDisposable) } return disposable } } public init(account: Account, peerId: PeerId) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId) }) } public func markAsSeen(opaqueId: Data) { self.impl.with { impl in impl.markAsSeen(opaqueId: opaqueId) } } public func markAction(opaqueId: Data) { self.impl.with { impl in impl.markAction(opaqueId: opaqueId) } } }