From eda5cd47ad7bc52235b0a9c683988b1263ff370d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 26 Sep 2021 21:24:47 +0300 Subject: [PATCH] Added manager of sponsored messages. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/data/data_session.cpp | 5 +- Telegram/SourceFiles/data/data_session.h | 5 + .../data/data_sponsored_messages.cpp | 196 ++++++++++++++++++ .../data/data_sponsored_messages.h | 79 +++++++ Telegram/SourceFiles/data/data_types.h | 3 + Telegram/SourceFiles/history/history.cpp | 3 + Telegram/SourceFiles/history/history_item.cpp | 6 +- Telegram/SourceFiles/history/history_item.h | 1 + .../history/view/history_view_element.cpp | 3 + 10 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 Telegram/SourceFiles/data/data_sponsored_messages.cpp create mode 100644 Telegram/SourceFiles/data/data_sponsored_messages.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index c3548cb96..93b5217bb 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -453,6 +453,8 @@ PRIVATE data/data_shared_media.h data/data_sparse_ids.cpp data/data_sparse_ids.h + data/data_sponsored_messages.cpp + data/data_sponsored_messages.h data/data_streaming.cpp data/data_streaming.h data/data_types.cpp diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index d024de60d..37ff099bc 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -53,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_scheduled_messages.h" #include "data/data_send_action.h" +#include "data/data_sponsored_messages.h" #include "data/data_cloud_themes.h" #include "data/data_streaming.h" #include "data/data_media_rotation.h" @@ -239,7 +240,8 @@ Session::Session(not_null session) , _streaming(std::make_unique(this)) , _mediaRotation(std::make_unique()) , _histories(std::make_unique(this)) -, _stickers(std::make_unique(this)) { +, _stickers(std::make_unique(this)) +, _sponsoredMessages(std::make_unique(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); @@ -280,6 +282,7 @@ void Session::clear() { _histories->unloadAll(); _scheduledMessages = nullptr; + _sponsoredMessages = nullptr; _dependentMessages.clear(); base::take(_messages); base::take(_channelMessages); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 9a0d991f6..5f32a167d 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -51,6 +51,7 @@ class LocationPoint; class WallPaper; class ScheduledMessages; class SendActionManager; +class SponsoredMessages; class ChatFilters; class CloudThemes; class Streaming; @@ -109,6 +110,9 @@ public: [[nodiscard]] Stickers &stickers() const { return *_stickers; } + [[nodiscard]] SponsoredMessages &sponsoredMessages() const { + return *_sponsoredMessages; + } [[nodiscard]] MsgId nextNonHistoryEntryId() { return ++_nonHistoryEntryId; } @@ -963,6 +967,7 @@ private: std::unique_ptr _mediaRotation; std::unique_ptr _histories; std::unique_ptr _stickers; + std::unique_ptr _sponsoredMessages; MsgId _nonHistoryEntryId = ServerMaxMsgId; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp new file mode 100644 index 000000000..448dea62a --- /dev/null +++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp @@ -0,0 +1,196 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_sponsored_messages.h" + +#include "api/api_text_entities.h" +#include "apiwrap.h" +#include "base/unixtime.h" +#include "data/data_channel.h" +#include "data/data_peer_id.h" +#include "data/data_session.h" +#include "history/history.h" +#include "main/main_session.h" + +namespace Data { +namespace { + +constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000); + +[[nodiscard]] bool TooEarlyForRequest(crl::time received) { + return (received > 0) && (received + kRequestTimeLimit > crl::now()); +} + +} // namespace + +SponsoredMessages::SponsoredMessages(not_null owner) +: _session(&owner->session()) +, _clearTimer([=] { clearOldRequests(); }) { +} + +SponsoredMessages::~SponsoredMessages() { + for (const auto &request : _requests) { + _session->api().request(request.second.requestId).cancel(); + } +} + +void SponsoredMessages::clearOldRequests() { + const auto now = crl::now(); + while (true) { + const auto i = ranges::find_if(_requests, [&](const auto &value) { + const auto &request = value.second; + return !request.requestId + && (request.lastReceived + kRequestTimeLimit <= now); + }); + if (i == end(_requests)) { + break; + } + _requests.erase(i); + } +} + +bool SponsoredMessages::append(not_null history) { + const auto it = _data.find(history); + if (it == end(_data)) { + return false; + } + auto &list = it->second; + if (list.showedAll) { + return false; + } + + const auto entryIt = ranges::find_if(list.entries, [](const Entry &e) { + return e.item == nullptr; + }); + if (entryIt == end(list.entries)) { + list.showedAll = true; + return false; + } + + const auto flags = MessageFlags(0) + | (history->isChannel() ? MessageFlag::Post : MessageFlags(0)) + | MessageFlag::HasFromId + | MessageFlag::IsSponsored + | MessageFlag::LocalHistoryEntry; + auto local = history->addNewLocalMessage( + _session->data().nextLocalMessageId(), + flags, + UserId(0), + MsgId(0), + HistoryItem::NewMessageDate(0), + entryIt->sponsored.fromId, + QString(), + entryIt->sponsored.textWithEntities, + MTP_messageMediaEmpty(), + HistoryMessageMarkupData()); + entryIt->item.reset(std::move(local)); + + // Since sponsored posts are only created on demand for display, + // we can send a request to view immediately. + view(entryIt); + + return true; +} + +void SponsoredMessages::request(not_null history) { + auto &request = _requests[history]; + if (request.requestId || TooEarlyForRequest(request.lastReceived)) { + return; + } + request.requestId = _session->api().request( + MTPchannels_GetSponsoredMessages( + _session->data().channel(history->channelId())->inputChannel + )).done([=](const MTPmessages_sponsoredMessages &result) { + parse(history, result); + }).fail([=](const MTP::Error &error) { + _requests.remove(history); + }).send(); +} + +void SponsoredMessages::parse( + not_null history, + const MTPmessages_sponsoredMessages &list) { + auto &request = _requests[history]; + request.lastReceived = crl::now(); + request.requestId = 0; + if (!_clearTimer.isActive()) { + _clearTimer.callOnce(kRequestTimeLimit * 2); + } + + list.match([&](const MTPDmessages_sponsoredMessages &data) { + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + + const auto &messages = data.vmessages().v; + if (messages.isEmpty()) { + return; + } + auto &list = _data.emplace(history, List()).first->second; + list.entries.clear(); + for (const auto &message : messages) { + append(history, list, message); + } + }); +} + +void SponsoredMessages::append( + not_null history, + List &list, + const MTPSponsoredMessage &message) { + message.match([&](const MTPDsponsoredMessage &data) { + const auto randomId = data.vrandom_id().v; + auto sharedMessage = SponsoredMessage{ + .randomId = randomId, + .fromId = peerFromMTP(data.vfrom_id()), + .textWithEntities = { + .text = qs(data.vmessage()), + .entities = Api::EntitiesFromMTP( + _session, + data.ventities().value_or_empty()), + }, + .history = history, + }; + list.entries.push_back({ nullptr, std::move(sharedMessage) }); + }); +} + +void SponsoredMessages::clearItems(not_null history) { + const auto it = _data.find(history); + if (it == end(_data)) { + return; + } + auto &list = it->second; + for (auto &entry : list.entries) { + entry.item.reset(); + } + list.showedAll = false; +} + +void SponsoredMessages::view(const std::vector::iterator entryIt) { + const auto randomId = entryIt->sponsored.randomId; + auto &request = _viewRequests[randomId]; + if (request.requestId || TooEarlyForRequest(request.lastReceived)) { + return; + } + const auto history = entryIt->sponsored.history; + if (!history) { + return; + } + request.requestId = _session->api().request( + MTPchannels_ViewSponsoredMessage( + _session->data().channel(history->channelId())->inputChannel, + MTP_bytes(randomId) + )).done([=] { + auto &request = _viewRequests[randomId]; + request.lastReceived = crl::now(); + request.requestId = 0; + }).fail([=](const MTP::Error &error) { + _viewRequests.remove(randomId); + }).send(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.h b/Telegram/SourceFiles/data/data_sponsored_messages.h new file mode 100644 index 000000000..b6115e59b --- /dev/null +++ b/Telegram/SourceFiles/data/data_sponsored_messages.h @@ -0,0 +1,79 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "history/history_item.h" +#include "base/timer.h" + +class History; + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class Session; + +struct SponsoredMessage final { + QByteArray randomId; + PeerId fromId; + TextWithEntities textWithEntities; + History *history = nullptr; +}; + +class SponsoredMessages final { +public: + using RandomId = QByteArray; + explicit SponsoredMessages(not_null owner); + SponsoredMessages(const SponsoredMessages &other) = delete; + SponsoredMessages &operator=(const SponsoredMessages &other) = delete; + ~SponsoredMessages(); + + void request(not_null history); + [[nodiscard]] bool append(not_null history); + void clearItems(not_null history); + +private: + using OwnedItem = std::unique_ptr; + struct Entry { + OwnedItem item; + SponsoredMessage sponsored; + }; + struct List { + std::vector entries; + bool showedAll = false; + }; + struct Request { + mtpRequestId requestId = 0; + crl::time lastReceived = 0; + }; + + void parse( + not_null history, + const MTPmessages_sponsoredMessages &list); + void append( + not_null history, + List &list, + const MTPSponsoredMessage &message); + void clearOldRequests(); + + void view(const std::vector::iterator entryIt); + + const not_null _session; + + base::Timer _clearTimer; + base::flat_map, List> _data; + base::flat_map, Request> _requests; + base::flat_map _viewRequests; + + rpl::lifetime _lifetime; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 5984ae624..d4ff2b973 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -275,6 +275,9 @@ enum class MessageFlag : uint32 { // Contact sign-up message, notification should be skipped for Silent. IsContactSignUp = (1U << 28), + + // In channels. + IsSponsored = (1U << 29), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index b4beaa22a..95000dde6 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2262,6 +2262,9 @@ void History::setChatListMessage(HistoryItem *item) { } const auto was = _chatListMessage.value_or(nullptr); if (item) { + if (item->isSponsored()) { + return; + } if (_chatListMessage && *_chatListMessage && !IsServerMsgId((*_chatListMessage)->id) diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index a8dfdf327..0da6cd111 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -452,6 +452,10 @@ bool HistoryItem::isScheduled() const { && (_flags & MessageFlag::IsOrWasScheduled); } +bool HistoryItem::isSponsored() const { + return (_flags & MessageFlag::IsSponsored); +} + bool HistoryItem::skipNotification() const { if (isSilent() && (_flags & MessageFlag::IsContactSignUp)) { return true; @@ -749,7 +753,7 @@ MsgId HistoryItem::replyToTop() const { } not_null HistoryItem::author() const { - return isPost() ? history()->peer : from(); + return (isPost() && !isSponsored()) ? history()->peer : from(); } TimeId HistoryItem::dateOriginal() const { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index e00ba3791..4a0c129d2 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -122,6 +122,7 @@ public: [[nodiscard]] bool isAdminLogEntry() const; [[nodiscard]] bool isFromScheduled() const; [[nodiscard]] bool isScheduled() const; + [[nodiscard]] bool isSponsored() const; [[nodiscard]] bool skipNotification() const; void addLogEntryOriginal( diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index fef37d398..6d3ccab22 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -658,6 +658,9 @@ void Element::recountDisplayDateInBlocks() { if (isHidden() || item->isEmpty()) { return false; } + if (item->isSponsored()) { + return false; + } if (const auto previous = previousDisplayedInBlocks()) { const auto prev = previous->data();