/* 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 "history/view/controls/history_view_webpage_processor.h" #include "base/unixtime.h" #include "data/data_chat_participant_status.h" #include "data/data_file_origin.h" #include "data/data_session.h" #include "data/data_web_page.h" #include "history/history.h" #include "lang/lang_keys.h" #include "main/main_session.h" namespace HistoryView::Controls { WebPageText TitleAndDescriptionFromWebPage(not_null d) { QString resultTitle, resultDescription; const auto document = d->document; const auto author = d->author; const auto siteName = d->siteName; const auto title = d->title; const auto description = d->description; const auto filenameOrUrl = [&] { return ((document && !document->filename().isEmpty()) ? document->filename() : d->url); }; const auto authorOrFilename = [&] { return (author.isEmpty() ? filenameOrUrl() : author); }; const auto descriptionOrAuthor = [&] { return (description.text.isEmpty() ? authorOrFilename() : description.text); }; if (siteName.isEmpty()) { if (title.isEmpty()) { if (description.text.isEmpty()) { resultTitle = author; resultDescription = filenameOrUrl(); } else { resultTitle = description.text; resultDescription = authorOrFilename(); } } else { resultTitle = title; resultDescription = descriptionOrAuthor(); } } else { resultTitle = siteName; resultDescription = title.isEmpty() ? descriptionOrAuthor() : title; } return { resultTitle, resultDescription }; } bool DrawWebPageDataPreview( QPainter &p, not_null webpage, not_null context, QRect to) { const auto document = webpage->document; const auto photo = webpage->photo; if ((!photo || photo->isNull()) && (!document || !document->hasThumbnail() || document->isPatternWallPaper())) { return false; } const auto preview = photo ? photo->getReplyPreview(Data::FileOrigin(), context, false) : document->getReplyPreview(Data::FileOrigin(), context, false); if (preview) { const auto w = preview->width(); const auto h = preview->height(); if (w == h) { p.drawPixmap(to.x(), to.y(), preview->pix()); } else { const auto from = (w > h) ? QRect((w - h) / 2, 0, h, h) : QRect(0, (h - w) / 2, w, w); p.drawPixmap(to, preview->pix(), from); } } return true; } [[nodiscard]] bool ShowWebPagePreview(WebPageData *page) { return page && !page->failed; } WebPageText ProcessWebPageData(WebPageData *page) { auto previewText = TitleAndDescriptionFromWebPage(page); if (previewText.title.isEmpty()) { if (page->document) { previewText.title = tr::lng_attach_file(tr::now); } else if (page->photo) { previewText.title = tr::lng_attach_photo(tr::now); } } return previewText; } WebpageResolver::WebpageResolver(not_null session) : _session(session) , _api(&session->mtp()) { } std::optional WebpageResolver::lookup( const QString &link) const { const auto i = _cache.find(link); return (i == end(_cache)) ? std::optional() : (i->second && !i->second->failed) ? i->second : nullptr; } QString WebpageResolver::find(not_null page) const { for (const auto &[link, cached] : _cache) { if (cached == page) { return link; } } return QString(); } void WebpageResolver::request(const QString &link, bool force) { if (_requestLink == link && !force) { return; } const auto done = [=](const MTPDmessageMediaWebPage &data) { const auto page = _session->data().processWebpage(data.vwebpage()); if (page->pendingTill > 0 && page->pendingTill < base::unixtime::now()) { page->pendingTill = 0; page->failed = true; } _cache.emplace(link, page->failed ? nullptr : page.get()); _resolved.fire_copy(link); }; const auto fail = [=] { _cache.emplace(link, nullptr); _resolved.fire_copy(link); }; _requestLink = link; _requestId = _api.request( MTPmessages_GetWebPagePreview( MTP_flags(0), MTP_string(link), MTPVector() )).done([=](const MTPMessageMedia &result, mtpRequestId requestId) { if (_requestId == requestId) { _requestId = 0; } result.match([=](const MTPDmessageMediaWebPage &data) { done(data); }, [&](const auto &d) { fail(); }); }).fail([=](const MTP::Error &error, mtpRequestId requestId) { if (_requestId == requestId) { _requestId = 0; } fail(); }).send(); } void WebpageResolver::cancel(const QString &link) { if (_requestLink == link) { _api.request(base::take(_requestId)).cancel(); } } WebpageProcessor::WebpageProcessor( not_null history, not_null field) : _history(history) , _resolver(std::make_shared(&history->session())) , _parser(field) , _timer([=] { if (!ShowWebPagePreview(_data) || _link.isEmpty()) { return; } _resolver->request(_link, true); }) { _history->session().downloaderTaskFinished( ) | rpl::filter([=] { return _data && (_data->document || _data->photo); }) | rpl::start_with_next([=] { _repaintRequests.fire({}); }, _lifetime); _history->owner().webPageUpdates( ) | rpl::filter([=](not_null page) { return (_data == page.get()); }) | rpl::start_with_next([=] { updateFromData(); }, _lifetime); _parser.list().changes( ) | rpl::start_with_next([=](QStringList &&parsed) { _parsedLinks = std::move(parsed); checkPreview(); }, _lifetime); _resolver->resolved() | rpl::start_with_next([=](QString link) { if (_link != link || _draft.removed || (_draft.manual && _draft.url != link)) { return; } _data = _resolver->lookup(link).value_or(nullptr); if (_data) { _draft.id = _data->id; _draft.url = _data->url; updateFromData(); } else { _links = QStringList(); checkPreview(); } }, _lifetime); } rpl::producer<> WebpageProcessor::repaintRequests() const { return _repaintRequests.events(); } Data::WebPageDraft WebpageProcessor::draft() const { return _draft; } std::shared_ptr WebpageProcessor::resolver() const { return _resolver; } const std::vector &WebpageProcessor::links() const { return _parser.ranges(); } QString WebpageProcessor::link() const { return _link; } void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) { const auto was = _link; if (draft.removed) { _draft = draft; if (_parsedLinks.empty()) { _draft.removed = false; } _data = nullptr; _links = QStringList(); _link = QString(); _parsed = WebpageParsed(); updateFromData(); } else if (draft.manual && !draft.url.isEmpty()) { _draft = draft; _parsedLinks = QStringList(); _links = QStringList(); _link = _draft.url; const auto page = draft.id ? _history->owner().webpage(draft.id).get() : nullptr; if (page && page->url == draft.url) { _data = page; if (const auto link = _resolver->find(page); !link.isEmpty()) { _link = link; } updateFromData(); } else { _resolver->request(_link); return; } } else if (!draft.manual && !_draft.manual) { _draft = draft; checkNow(reparse); } if (_link != was) { _resolver->cancel(was); } } void WebpageProcessor::updateFromData() { _timer.cancel(); auto parsed = WebpageParsed(); if (ShowWebPagePreview(_data)) { if (const auto till = _data->pendingTill) { parsed.drawPreview = [](QPainter &p, QRect to) { return false; }; parsed.title = tr::lng_preview_loading(tr::now); parsed.description = _link; const auto timeout = till - base::unixtime::now(); _timer.callOnce( std::max(timeout, 0) * crl::time(1000)); } else { const auto webpage = _data; const auto context = _history->peer; const auto preview = ProcessWebPageData(_data); parsed.title = preview.title; parsed.description = preview.description; parsed.drawPreview = [=](QPainter &p, QRect to) { return DrawWebPageDataPreview(p, webpage, context, to); }; } } _parsed = std::move(parsed); _repaintRequests.fire({}); } void WebpageProcessor::setDisabled(bool disabled) { _parser.setDisabled(disabled); if (disabled) { apply({ .removed = true }); } else { checkNow(false); } } void WebpageProcessor::checkNow(bool force) { _parser.parseNow(); if (force) { _link = QString(); _links = QStringList(); if (_parsedLinks.isEmpty()) { _data = nullptr; updateFromData(); return; } } checkPreview(); } void WebpageProcessor::checkPreview() { const auto previewRestricted = _history->peer && _history->peer->amRestricted(ChatRestriction::EmbedLinks); if (_parsedLinks.empty()) { _draft.removed = false; } if (_draft.removed) { return; } else if (previewRestricted) { apply({ .removed = true }); _draft.removed = false; return; } else if (_draft.manual) { return; } else if (_links == _parsedLinks) { return; } _links = _parsedLinks; auto page = (WebPageData*)nullptr; auto chosen = QString(); for (const auto &link : _links) { const auto value = _resolver->lookup(link); if (!value) { chosen = link; break; } else if (*value) { chosen = link; page = *value; break; } } if (_link != chosen) { _resolver->cancel(_link); _link = chosen; if (!page && !_link.isEmpty()) { _resolver->request(_link); } } if (page) { _data = page; _draft.id = _data->id; _draft.url = _data->url; } else { _data = nullptr; _draft = {}; } updateFromData(); } rpl::producer WebpageProcessor::parsedValue() const { return _parsed.value(); } } // namespace HistoryView::Controls