2024-08-13 09:49:23 -03:00

626 lines
26 KiB
Swift

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 title
case text
case textEntities
case media
case contentMedia
case color
case backgroundEmojiId
case url
case buttonText
case sponsorInfo
case additionalInfo
case canReport
}
enum MessageType: Int32, Codable {
case sponsored = 0
case recommended = 1
}
public let opaqueId: Data
public let messageType: MessageType
public let title: String
public let text: String
public let textEntities: [MessageTextEntity]
public let media: [Media]
public let contentMedia: [Media]
public let color: PeerNameColor?
public let backgroundEmojiId: Int64?
public let url: String
public let buttonText: String
public let sponsorInfo: String?
public let additionalInfo: String?
public let canReport: Bool
public init(
opaqueId: Data,
messageType: MessageType,
title: String,
text: String,
textEntities: [MessageTextEntity],
media: [Media],
contentMedia: [Media],
color: PeerNameColor?,
backgroundEmojiId: Int64?,
url: String,
buttonText: String,
sponsorInfo: String?,
additionalInfo: String?,
canReport: Bool
) {
self.opaqueId = opaqueId
self.messageType = messageType
self.title = title
self.text = text
self.textEntities = textEntities
self.media = media
self.contentMedia = contentMedia
self.color = color
self.backgroundEmojiId = backgroundEmojiId
self.url = url
self.buttonText = buttonText
self.sponsorInfo = sponsorInfo
self.additionalInfo = additionalInfo
self.canReport = canReport
}
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.title = try container.decode(String.self, forKey: .title)
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
}
let contentMediaData = try container.decode([Data].self, forKey: .contentMedia)
self.contentMedia = contentMediaData.compactMap { data -> Media? in
return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media
}
self.color = try container.decodeIfPresent(Int32.self, forKey: .color).flatMap { PeerNameColor(rawValue: $0) }
self.backgroundEmojiId = try container.decodeIfPresent(Int64.self, forKey: .backgroundEmojiId)
self.url = try container.decode(String.self, forKey: .url)
self.buttonText = try container.decode(String.self, forKey: .buttonText)
self.sponsorInfo = try container.decodeIfPresent(String.self, forKey: .sponsorInfo)
self.additionalInfo = try container.decodeIfPresent(String.self, forKey: .additionalInfo)
self.canReport = try container.decodeIfPresent(Bool.self, forKey: .canReport) ?? false
}
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.title, forKey: .title)
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)
let contentMediaData = self.contentMedia.map { media -> Data in
let encoder = PostboxEncoder()
encoder.encodeRootObject(media)
return encoder.makeData()
}
try container.encode(contentMediaData, forKey: .contentMedia)
try container.encodeIfPresent(self.color?.rawValue, forKey: .color)
try container.encodeIfPresent(self.backgroundEmojiId, forKey: .backgroundEmojiId)
try container.encode(self.url, forKey: .url)
try container.encode(self.buttonText, forKey: .buttonText)
try container.encodeIfPresent(self.sponsorInfo, forKey: .sponsorInfo)
try container.encodeIfPresent(self.additionalInfo, forKey: .additionalInfo)
try container.encode(self.canReport, forKey: .canReport)
}
public static func ==(lhs: CachedMessage, rhs: CachedMessage) -> Bool {
if lhs.opaqueId != rhs.opaqueId {
return false
}
if lhs.messageType != rhs.messageType {
return false
}
if lhs.title != rhs.title {
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.contentMedia.count != rhs.contentMedia.count {
return false
}
for i in 0 ..< lhs.contentMedia.count {
if !lhs.contentMedia[i].isEqual(to: rhs.contentMedia[i]) {
return false
}
}
if lhs.url != rhs.url {
return false
}
if lhs.buttonText != rhs.buttonText {
return false
}
if lhs.sponsorInfo != rhs.sponsorInfo {
return false
}
if lhs.additionalInfo != rhs.additionalInfo {
return false
}
if lhs.canReport != rhs.canReport {
return false
}
return true
}
func toMessage(peerId: PeerId, transaction: Transaction) -> Message? {
var attributes: [MessageAttribute] = []
let mappedMessageType: AdMessageAttribute.MessageType
switch self.messageType {
case .sponsored:
mappedMessageType = .sponsored
case .recommended:
mappedMessageType = .recommended
}
attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, messageType: mappedMessageType, url: self.url, buttonText: self.buttonText, sponsorInfo: self.sponsorInfo, additionalInfo: self.additionalInfo, canReport: self.canReport, hasContentMedia: !self.contentMedia.isEmpty))
if !self.textEntities.isEmpty {
let attribute = TextEntitiesMessageAttribute(entities: self.textEntities)
attributes.append(attribute)
}
var messagePeers = SimpleDictionary<PeerId, Peer>()
if let peer = transaction.getPeer(peerId) {
messagePeers[peer.id] = peer
}
let author: Peer = TelegramChannel(
id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)),
accessHash: nil,
title: self.title,
username: nil,
photo: [],
creationDate: 0,
version: 0,
participationStatus: .left,
info: .broadcast(TelegramChannelBroadcastInfo(flags: [])),
flags: [],
restrictionInfo: nil,
adminRights: nil,
bannedRights: nil,
defaultBannedRights: nil,
usernames: [],
storiesHidden: nil,
nameColor: self.color ?? .blue,
backgroundEmojiId: self.backgroundEmojiId,
profileColor: nil,
profileBackgroundEmojiId: nil,
emojiStatus: nil,
approximateBoostLevel: nil,
subscriptionUntilDate: nil
)
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: [],
customTags: [],
forwardInfo: nil,
author: author,
text: self.text,
attributes: attributes,
media: !self.contentMedia.isEmpty ? self.contentMedia : self.media,
peers: messagePeers,
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
}
}
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let maskAsSeenDisposables = DisposableDict<Data>()
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<CachedState?, NoError> {
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<State>()
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
let accountPeerId = account.peerId
self.stateValue = State(interPostInterval: nil, messages: [])
self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId)
|> mapToSignal { cachedState -> Signal<State, NoError> 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<Api.messages.SponsoredMessages?, NoError> 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):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var parsedMessages: [CachedMessage] = []
for message in messages {
switch message {
case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo):
var parsedEntities: [MessageTextEntity] = []
if let entities = entities {
parsedEntities = messageTextEntitiesFromApiEntities(entities)
}
let isRecommended = (flags & (1 << 5)) != 0
let canReport = (flags & (1 << 12)) != 0
var nameColorIndex: Int32?
var backgroundEmojiId: Int64?
if let color = color {
switch color {
case let .peerColor(_, color, backgroundEmojiIdValue):
nameColorIndex = color
backgroundEmojiId = backgroundEmojiIdValue
}
}
let photo = photo.flatMap { telegramMediaImageFromApiPhoto($0) }
let (contentMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
parsedMessages.append(CachedMessage(
opaqueId: randomId.makeData(),
messageType: isRecommended ? .recommended : .sponsored,
title: title,
text: message,
textEntities: parsedEntities,
media: photo.flatMap { [$0] } ?? [],
contentMedia: contentMedia.flatMap { [$0] } ?? [],
color: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) },
backgroundEmojiId: backgroundEmojiId,
url: url,
buttonText: buttonText,
sponsorInfo: sponsorInfo,
additionalInfo: additionalInfo,
canReport: canReport
))
}
}
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<Never, NoError> = account.postbox.transaction { transaction -> Api.InputChannel? in
return transaction.getPeer(self.peerId).flatMap(apiInputChannel)
}
|> mapToSignal { inputChannel -> Signal<Never, NoError> 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<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
self.maskAsSeenDisposables.set(signal.start(), forKey: opaqueId)
}
func markAction(opaqueId: Data) {
_internal_markAdAction(account: self.account, peerId: self.peerId, opaqueId: opaqueId)
}
func remove(opaqueId: Data) {
if var stateValue = self.stateValue {
if let index = stateValue.messages.firstIndex(where: { $0.adAttribute?.opaqueId == opaqueId }) {
stateValue.messages.remove(at: index)
self.stateValue = stateValue
}
}
let peerId = self.peerId
let _ = (self.account.postbox.transaction { transaction -> Void in
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
let id = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key)
guard var cachedState = transaction.retrieveItemCacheEntry(id: id)?.get(CachedState.self) else {
return
}
if let index = cachedState.messages.firstIndex(where: { $0.opaqueId == opaqueId }) {
cachedState.messages.remove(at: index)
if let entry = CodableEntry(cachedState) {
transaction.putItemCacheEntry(id: id, entry: entry)
}
}
}).start()
}
}
public class AdMessagesHistoryContext {
private let queue = Queue()
private let impl: QueueLocalObject<AdMessagesHistoryContextImpl>
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)
}
}
public func remove(opaqueId: Data) {
self.impl.with { impl in
impl.remove(opaqueId: opaqueId)
}
}
}
func _internal_markAdAction(account: Account, peerId: EnginePeer.Id, opaqueId: Data) {
let signal: Signal<Never, NoError> = account.postbox.transaction { transaction -> Api.InputChannel? in
return transaction.getPeer(peerId).flatMap(apiInputChannel)
}
|> mapToSignal { inputChannel -> Signal<Never, NoError> in
guard let inputChannel = inputChannel else {
return .complete()
}
return account.network.request(Api.functions.channels.clickSponsoredMessage(channel: inputChannel, randomId: Buffer(data: opaqueId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
let _ = signal.start()
}