From b299881bf86ada72f246af54eed51c2d87634561 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 21 May 2024 18:15:56 +0400
Subject: [PATCH] Track factcheck text and create media.

---
 .../data/components/factchecks.cpp            | 136 ++++++++++++++++++
 .../SourceFiles/data/components/factchecks.h  |  51 +++++++
 Telegram/SourceFiles/data/data_session.cpp    |  20 ++-
 .../admin_log/history_admin_log_inner.cpp     |   2 +
 Telegram/SourceFiles/history/history_item.cpp |  34 +++++
 Telegram/SourceFiles/history/history_item.h   |   4 +
 .../history/history_item_components.cpp       |  25 ++++
 .../history/history_item_components.h         |  25 ++++
 .../SourceFiles/history/history_item_text.cpp |  11 ++
 .../history/view/history_view_message.cpp     |  23 +++
 .../history/view/history_view_message.h       |   5 +
 Telegram/SourceFiles/main/main_session.cpp    |   2 +
 Telegram/SourceFiles/main/main_session.h      |   5 +
 13 files changed, 332 insertions(+), 11 deletions(-)
 create mode 100644 Telegram/SourceFiles/data/components/factchecks.cpp
 create mode 100644 Telegram/SourceFiles/data/components/factchecks.h

diff --git a/Telegram/SourceFiles/data/components/factchecks.cpp b/Telegram/SourceFiles/data/components/factchecks.cpp
new file mode 100644
index 000000000..4e5fcd090
--- /dev/null
+++ b/Telegram/SourceFiles/data/components/factchecks.cpp
@@ -0,0 +1,136 @@
+/*
+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/components/factchecks.h"
+
+#include "apiwrap.h"
+#include "base/random.h"
+#include "data/data_session.h"
+#include "history/view/media/history_view_web_page.h"
+#include "history/view/history_view_message.h"
+#include "history/history.h"
+#include "history/history_item.h"
+#include "history/history_item_components.h"
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+
+namespace Data {
+namespace {
+
+constexpr auto kRequestDelay = crl::time(1000);
+
+} // namespace
+
+Factchecks::Factchecks(not_null<Main::Session*> session)
+: _session(session)
+, _requestTimer([=] { request(); }) {
+}
+
+void Factchecks::requestFor(not_null<HistoryItem*> item) {
+	subscribeIfNotYet();
+
+	if (const auto factcheck = item->Get<HistoryMessageFactcheck>()) {
+		factcheck->requested = true;
+	}
+	if (!_requestTimer.isActive()) {
+		_requestTimer.callOnce(kRequestDelay);
+	}
+	const auto changed = !_pending.empty()
+		&& (_pending.front()->history() != item->history());
+	const auto added = _pending.emplace(item).second;
+	if (changed) {
+		request();
+	} else if (added && _pending.size() == 1) {
+		_requestTimer.callOnce(kRequestDelay);
+	}
+}
+
+void Factchecks::subscribeIfNotYet() {
+	if (_subscribed) {
+		return;
+	}
+	_subscribed = true;
+
+	_session->data().itemRemoved(
+	) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
+		_pending.remove(item);
+		const auto i = ranges::find(_requested, item.get());
+		if (i != end(_requested)) {
+			*i = nullptr;
+		}
+	}, _lifetime);
+}
+
+void Factchecks::request() {
+	_requestTimer.cancel();
+
+	if (!_requested.empty() || _pending.empty()) {
+		return;
+	}
+	_session->api().request(base::take(_requestId)).cancel();
+
+	auto ids = QVector<MTPint>();
+	ids.reserve(_pending.size());
+	const auto history = _pending.front()->history();
+	for (auto i = begin(_pending); i != end(_pending);) {
+		const auto &item = *i;
+		if (item->history() == history) {
+			_requested.push_back(item);
+			ids.push_back(MTP_int(item->id.bare));
+			i = _pending.erase(i);
+		} else {
+			++i;
+		}
+	}
+	_requestId = _session->api().request(MTPmessages_GetFactCheck(
+		history->peer->input,
+		MTP_vector<MTPint>(std::move(ids))
+	)).done([=](const MTPVector<MTPFactCheck> &result) {
+		_requestId = 0;
+		const auto &list = result.v;
+		auto index = 0;
+		for (const auto &item : base::take(_requested)) {
+			if (!item) {
+			} else if (index >= list.size()) {
+				item->setFactcheck({});
+			} else {
+				item->setFactcheck(FromMTP(item, &list[index]));
+			}
+			++index;
+		}
+		if (!_pending.empty()) {
+			request();
+		}
+	}).fail([=] {
+		_requestId = 0;
+		for (const auto &item : base::take(_requested)) {
+			if (item) {
+				item->setFactcheck({});
+			}
+		}
+		if (!_pending.empty()) {
+			request();
+		}
+	}).send();
+}
+
+std::unique_ptr<HistoryView::WebPage> Factchecks::makeMedia(
+		not_null<HistoryView::Message*> view,
+		not_null<HistoryMessageFactcheck*> factcheck) {
+	if (!factcheck->page) {
+		factcheck->page = view->history()->owner().webpage(
+			base::RandomValue<WebPageId>(),
+			tr::lng_factcheck_title(tr::now),
+			factcheck->data.text);
+	}
+	return std::make_unique<HistoryView::WebPage>(
+		view,
+		factcheck->page,
+		MediaWebPageFlags());
+}
+
+} // namespace Data
diff --git a/Telegram/SourceFiles/data/components/factchecks.h b/Telegram/SourceFiles/data/components/factchecks.h
new file mode 100644
index 000000000..452706f9b
--- /dev/null
+++ b/Telegram/SourceFiles/data/components/factchecks.h
@@ -0,0 +1,51 @@
+/*
+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 "base/timer.h"
+
+class HistoryItem;
+struct HistoryMessageFactcheck;
+
+namespace HistoryView {
+class Message;
+class WebPage;
+} // namespace HistoryView
+
+namespace Main {
+class Session;
+} // namespace Main
+
+namespace Data {
+
+class Factchecks final {
+public:
+	explicit Factchecks(not_null<Main::Session*> session);
+
+	void requestFor(not_null<HistoryItem*> item);
+	[[nodiscard]] std::unique_ptr<HistoryView::WebPage> makeMedia(
+		not_null<HistoryView::Message*> view,
+		not_null<HistoryMessageFactcheck*> factcheck);
+
+private:
+	void subscribeIfNotYet();
+	void request();
+
+	const not_null<Main::Session*> _session;
+
+	base::Timer _requestTimer;
+	base::flat_set<not_null<HistoryItem*>> _pending;
+	std::vector<HistoryItem*> _requested;
+	mtpRequestId _requestId = 0;
+	bool _subscribed = false;
+
+	rpl::lifetime _lifetime;
+
+};
+
+} // namespace Data
diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp
index d101d5d88..529b933d4 100644
--- a/Telegram/SourceFiles/data/data_session.cpp
+++ b/Telegram/SourceFiles/data/data_session.cpp
@@ -4250,29 +4250,27 @@ void Session::notifyPollUpdateDelayed(not_null<PollData*> poll) {
 }
 
 void Session::sendWebPageGamePollNotifications() {
+	auto resize = std::vector<not_null<ViewElement*>>();
 	for (const auto &page : base::take(_webpagesUpdated)) {
 		_webpageUpdates.fire_copy(page);
-		const auto i = _webpageViews.find(page);
-		if (i != _webpageViews.end()) {
-			for (const auto &view : i->second) {
-				requestViewResize(view);
-			}
+		if (const auto i = _webpageViews.find(page)
+			; i != _webpageViews.end()) {
+			resize.insert(end(resize), begin(i->second), end(i->second));
 		}
 	}
 	for (const auto &game : base::take(_gamesUpdated)) {
 		if (const auto i = _gameViews.find(game); i != _gameViews.end()) {
-			for (const auto &view : i->second) {
-				requestViewResize(view);
-			}
+			resize.insert(end(resize), begin(i->second), end(i->second));
 		}
 	}
 	for (const auto &poll : base::take(_pollsUpdated)) {
 		if (const auto i = _pollViews.find(poll); i != _pollViews.end()) {
-			for (const auto &view : i->second) {
-				requestViewResize(view);
-			}
+			resize.insert(end(resize), begin(i->second), end(i->second));
 		}
 	}
+	for (const auto &view : resize) {
+		requestViewResize(view);
+	}
 }
 
 rpl::producer<not_null<WebPageData*>> Session::webPageUpdates() const {
diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
index 61cf9a889..8fa440bf6 100644
--- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
+++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp
@@ -1306,6 +1306,8 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
 					&& !link
 					&& (view->hasVisibleText()
 						|| mediaHasTextForCopy
+						|| (item->Has<HistoryMessageFactcheck>()
+							&& !item->Get<HistoryMessageFactcheck>()->data.text.empty())
 						|| item->Has<HistoryMessageLogEntryOriginal>())) {
 					_menu->addAction(tr::lng_context_copy_text(tr::now), [=] {
 						copyContextText(itemId);
diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp
index 7cb74b189..3a2ed484a 100644
--- a/Telegram/SourceFiles/history/history_item.cpp
+++ b/Telegram/SourceFiles/history/history_item.cpp
@@ -415,6 +415,11 @@ HistoryItem::HistoryItem(
 		}
 		setReactions(data.vreactions());
 		applyTTL(data);
+
+		if (const auto check = FromMTP(this, data.vfactcheck())) {
+			AddComponents(HistoryMessageFactcheck::Bit());
+			Get<HistoryMessageFactcheck>()->data = check;
+		}
 	}
 }
 
@@ -1494,6 +1499,33 @@ void HistoryItem::addLogEntryOriginal(
 		content);
 }
 
+void HistoryItem::setFactcheck(MessageFactcheck info) {
+	if (!info) {
+		if (Has<HistoryMessageFactcheck>()) {
+			RemoveComponents(HistoryMessageFactcheck::Bit());
+			history()->owner().requestItemResize(this);
+		}
+	} else {
+		AddComponents(HistoryMessageFactcheck::Bit());
+		const auto factcheck = Get<HistoryMessageFactcheck>();
+		if (factcheck->data.hash == info.hash
+			&& (info.needCheck || !factcheck->data.needCheck)) {
+			return;
+		} else if (factcheck->data.text != info.text
+			|| factcheck->data.country != info.country
+			|| factcheck->data.hash != info.hash) {
+			factcheck->data = std::move(info);
+			factcheck->requested = false;
+			history()->owner().requestItemResize(this);
+		}
+	}
+}
+
+bool HistoryItem::hasUnrequestedFactcheck() const {
+	const auto factcheck = Get<HistoryMessageFactcheck>();
+	return factcheck && factcheck->data.needCheck && !factcheck->requested;
+}
+
 PeerData *HistoryItem::specialNotificationPeer() const {
 	return (mentionsMe() && !_history->peer->isUser())
 		? from().get()
@@ -3143,6 +3175,8 @@ EffectId HistoryItem::effectId() const {
 bool HistoryItem::isEmpty() const {
 	return _text.empty()
 		&& !_media
+		&& (!Has<HistoryMessageFactcheck>()
+			|| Get<HistoryMessageFactcheck>()->data.text.empty())
 		&& !Has<HistoryMessageLogEntryOriginal>();
 }
 
diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h
index 1b006e35a..5a864d076 100644
--- a/Telegram/SourceFiles/history/history_item.h
+++ b/Telegram/SourceFiles/history/history_item.h
@@ -23,9 +23,11 @@ struct HistoryMessageReplyMarkup;
 struct HistoryMessageTranslation;
 struct HistoryMessageForwarded;
 struct HistoryMessageSavedMediaData;
+struct HistoryMessageFactcheck;
 struct HistoryServiceDependentData;
 enum class HistorySelfDestructType;
 struct PreparedServiceText;
+struct MessageFactcheck;
 class ReplyKeyboard;
 struct LanguageId;
 
@@ -204,6 +206,8 @@ public:
 		WebPageId localId,
 		const QString &label,
 		const TextWithEntities &content);
+	void setFactcheck(MessageFactcheck info);
+	[[nodiscard]] bool hasUnrequestedFactcheck() const;
 
 	[[nodiscard]] not_null<Data::Thread*> notificationThread() const;
 	[[nodiscard]] not_null<History*> history() const {
diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp
index dc4a38c7a..54e42a16e 100644
--- a/Telegram/SourceFiles/history/history_item_components.cpp
+++ b/Telegram/SourceFiles/history/history_item_components.cpp
@@ -1062,6 +1062,31 @@ HistoryMessageLogEntryOriginal &HistoryMessageLogEntryOriginal::operator=(
 
 HistoryMessageLogEntryOriginal::~HistoryMessageLogEntryOriginal() = default;
 
+MessageFactcheck FromMTP(
+		not_null<HistoryItem*> item,
+		const tl::conditional<MTPFactCheck> &factcheck) {
+	auto result = MessageFactcheck();
+	if (!factcheck) {
+		return result;
+	}
+	const auto &data = factcheck->data();
+	if (const auto text = data.vtext()) {
+		const auto &data = text->data();
+		result.text = {
+			qs(data.vtext()),
+			Api::EntitiesFromMTP(
+				&item->history()->session(),
+				data.ventities().v),
+		};
+	}
+	if (const auto country = data.vcountry()) {
+		result.country = qs(country->v);
+	}
+	result.hash = data.vhash().v;
+	result.needCheck = data.is_need_check();
+	return result;
+}
+
 HistoryDocumentCaptioned::HistoryDocumentCaptioned()
 : caption(st::msgFileMinWidth - st::msgPadding.left() - st::msgPadding.right()) {
 }
diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h
index 44f31f3cd..fa7ffbf3a 100644
--- a/Telegram/SourceFiles/history/history_item_components.h
+++ b/Telegram/SourceFiles/history/history_item_components.h
@@ -562,6 +562,31 @@ struct HistoryMessageLogEntryOriginal
 
 };
 
+struct MessageFactcheck {
+	TextWithEntities text;
+	QString country;
+	uint64 hash = 0;
+	bool needCheck = false;
+
+	[[nodiscard]] bool empty() const {
+		return text.empty() && country.isEmpty() && !hash;
+	}
+	explicit operator bool() const {
+		return !empty();
+	}
+};
+
+[[nodiscard]] MessageFactcheck FromMTP(
+	not_null<HistoryItem*> item,
+	const tl::conditional<MTPFactCheck> &factcheck);
+
+struct HistoryMessageFactcheck
+: public RuntimeComponent<HistoryMessageFactcheck, HistoryItem> {
+	MessageFactcheck data;
+	WebPageData *page = nullptr;
+	bool requested = false;
+};
+
 struct HistoryServiceData
 : public RuntimeComponent<HistoryServiceData, HistoryItem> {
 	std::vector<ClickHandlerPtr> textLinks;
diff --git a/Telegram/SourceFiles/history/history_item_text.cpp b/Telegram/SourceFiles/history/history_item_text.cpp
index 1c66c59a4..ba35168cf 100644
--- a/Telegram/SourceFiles/history/history_item_text.cpp
+++ b/Telegram/SourceFiles/history/history_item_text.cpp
@@ -46,6 +46,12 @@ TextForMimeData HistoryItemText(not_null<HistoryItem*> item) {
 		titleResult.append('\n').append(std::move(descriptionResult));
 		return titleResult;
 	}();
+	auto factcheckResult = [&] {
+		const auto factcheck = item->Get<HistoryMessageFactcheck>();
+		return factcheck
+			? TextForMimeData::Rich(base::duplicate(factcheck->data.text))
+			: TextForMimeData();
+	}();
 	auto result = textResult;
 	if (result.empty()) {
 		result = std::move(mediaResult);
@@ -57,6 +63,11 @@ TextForMimeData HistoryItemText(not_null<HistoryItem*> item) {
 	} else if (!logEntryOriginalResult.empty()) {
 		result.append(u"\n\n"_q).append(std::move(logEntryOriginalResult));
 	}
+	if (result.empty()) {
+		result = std::move(factcheckResult);
+	} else if (!factcheckResult.empty()) {
+		result.append(u"\n\n"_q).append(std::move(factcheckResult));
+	}
 	return result;
 }
 
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index cf19c8f9a..861c96ee7 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/round_rect.h"
 #include "ui/text/text_utilities.h"
 #include "ui/power_saving.h"
+#include "data/components/factchecks.h"
 #include "data/components/sponsored_messages.h"
 #include "data/data_session.h"
 #include "data/data_user.h"
@@ -799,6 +800,24 @@ QSize Message::performCountOptimalSize() {
 		RemoveComponents(Reply::Bit());
 	}
 
+	const auto factcheck = item->Get<HistoryMessageFactcheck>();
+	if (factcheck && !factcheck->data.text.empty()) {
+		AddComponents(Factcheck::Bit());
+		Get<Factcheck>()->page = history()->session().factchecks().makeMedia(
+			this,
+			factcheck);
+
+		auto copy = data()->originalText();
+		if (!copy.text.contains("FACT CHECK")) {
+			copy.append("\n\nFACT CHECK!!\n\n").append(factcheck->data.text);
+			crl::on_main(this, [=] {
+				data()->setText(std::move(copy));
+			});
+		}
+	} else {
+		RemoveComponents(Factcheck::Bit());
+	}
+
 	const auto markup = item->inlineReplyMarkup();
 	const auto reactionsKey = [&] {
 		return embedReactionsInBottomInfo()
@@ -1069,6 +1088,10 @@ void Message::draw(Painter &p, const PaintContext &context) const {
 	const auto item = data();
 	const auto media = this->media();
 
+	if (item->hasUnrequestedFactcheck()) {
+		item->history()->session().factchecks().requestFor(item);
+	}
+
 	const auto stm = context.messageStyle();
 	const auto bubble = drawBubble();
 
diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h
index a8c172c03..5f64193d7 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.h
+++ b/Telegram/SourceFiles/history/view/history_view_message.h
@@ -44,6 +44,11 @@ struct LogEntryOriginal
 	std::unique_ptr<WebPage> page;
 };
 
+struct Factcheck
+: public RuntimeComponent<Factcheck, Element> {
+	std::unique_ptr<WebPage> page;
+};
+
 struct PsaTooltipState : public RuntimeComponent<PsaTooltipState, Element> {
 	QString type;
 	mutable ClickHandlerPtr link;
diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp
index fc8435d66..951ec735a 100644
--- a/Telegram/SourceFiles/main/main_session.cpp
+++ b/Telegram/SourceFiles/main/main_session.cpp
@@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "storage/file_upload.h"
 #include "storage/storage_account.h"
 #include "storage/storage_facade.h"
+#include "data/components/factchecks.h"
 #include "data/components/recent_peers.h"
 #include "data/components/scheduled_messages.h"
 #include "data/components/sponsored_messages.h"
@@ -105,6 +106,7 @@ Session::Session(
 , _scheduledMessages(std::make_unique<Data::ScheduledMessages>(this))
 , _sponsoredMessages(std::make_unique<Data::SponsoredMessages>(this))
 , _topPeers(std::make_unique<Data::TopPeers>(this))
+, _factchecks(std::make_unique<Data::Factchecks>(this))
 , _cachedReactionIconFactory(std::make_unique<ReactionIconFactory>())
 , _supportHelper(Support::Helper::Create(this))
 , _saveSettingsTimer([=] { saveSettings(); }) {
diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h
index 85aa2fe08..9581e7cd4 100644
--- a/Telegram/SourceFiles/main/main_session.h
+++ b/Telegram/SourceFiles/main/main_session.h
@@ -35,6 +35,7 @@ class RecentPeers;
 class ScheduledMessages;
 class SponsoredMessages;
 class TopPeers;
+class Factchecks;
 } // namespace Data
 
 namespace HistoryView::Reactions {
@@ -127,6 +128,9 @@ public:
 	[[nodiscard]] Data::TopPeers &topPeers() const {
 		return *_topPeers;
 	}
+	[[nodiscard]] Data::Factchecks &factchecks() const {
+		return *_factchecks;
+	}
 	[[nodiscard]] Api::Updates &updates() const {
 		return *_updates;
 	}
@@ -254,6 +258,7 @@ private:
 	const std::unique_ptr<Data::ScheduledMessages> _scheduledMessages;
 	const std::unique_ptr<Data::SponsoredMessages> _sponsoredMessages;
 	const std::unique_ptr<Data::TopPeers> _topPeers;
+	const std::unique_ptr<Data::Factchecks> _factchecks;
 
 	using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory;
 	const std::unique_ptr<ReactionIconFactory> _cachedReactionIconFactory;