/* 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 "iv/iv_instance.h" #include "apiwrap.h" #include "base/platform/base_platform_info.h" #include "base/unixtime.h" #include "boxes/share_box.h" #include "core/application.h" #include "core/file_utilities.h" #include "core/shortcuts.h" #include "core/click_handler_types.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_session.h" #include "data/data_web_page.h" #include "history/history.h" #include "history/history_item.h" #include "iv/markdown/iv_markdown_article.h" #include "iv/markdown/iv_markdown_controller.h" #include "iv/iv_cached_media.h" #include "iv/iv_controller.h" #include "iv/iv_data.h" #include "iv/iv_rich_page.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "main/session/session_show.h" #include "media/view/media_view_open_common.h" #include "storage/storage_account.h" #include "ui/toast/toast.h" #include "ui/basic_click_handlers.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_session_controller_link_info.h" #include "styles/style_iv.h" #include #include #include namespace Iv { namespace { constexpr auto kAllowPageReloadAfter = 3 * crl::time(1000); struct NativeIvChannelContext { uint64 channelId = 0; QString username; }; [[nodiscard]] NativeIvChannelContext ParseNativeIvChannelContext( const QString &context) { const auto separator = context.indexOf(u'\n'); return { .channelId = (separator >= 0) ? context.mid(0, separator).toULongLong() : context.toULongLong(), .username = (separator >= 0) ? context.mid(separator + 1) : QString(), }; } [[nodiscard]] QString ResolveNativeIvChannelUsername( const QString &channelUsername, const QString &contextUsername) { return !channelUsername.isEmpty() ? channelUsername : contextUsername; } struct MarkdownMessageContext { ClickHandlerContext clickHandlerContext; base::weak_ptr sessionWindow; }; struct LocalMarkdownTarget { QString key; QString path; QString sourceName; QString fragment; }; [[nodiscard]] QString NormalizeLocalMarkdownFragment(QString fragment) { fragment = QString::fromUtf8( QByteArray::fromPercentEncoding(fragment.toUtf8())); fragment = fragment.trimmed().toLower(); while (fragment.startsWith(QChar('#'))) { fragment.remove(0, 1); } return fragment; } [[nodiscard]] LocalMarkdownTarget ParseLocalMarkdownTarget(QString path) { auto sourcePath = path; auto fragment = QString(); if (!QFileInfo(sourcePath).exists()) { const auto hash = sourcePath.lastIndexOf(QChar('#')); const auto candidate = (hash > 0) ? sourcePath.mid(0, hash) : QString(); if (!candidate.isEmpty() && QFileInfo(candidate).exists()) { fragment = NormalizeLocalMarkdownFragment(sourcePath.mid(hash + 1)); sourcePath = candidate; } } const auto info = QFileInfo(sourcePath); if (!info.exists()) { return { .key = path, .path = std::move(path), }; } auto result = LocalMarkdownTarget{ .key = info.absoluteFilePath(), .path = info.absoluteFilePath(), .sourceName = info.fileName(), .fragment = std::move(fragment), }; if (!result.fragment.isEmpty()) { result.path += u"#"_q + result.fragment; } return result; } [[nodiscard]] auto ExtractMarkdownMessageContext(const QVariant &context) { if (!context.isValid() || !context.canConvert()) { return std::optional(); } const auto clickHandlerContext = context.value(); return std::make_optional(MarkdownMessageContext{ .clickHandlerContext = clickHandlerContext, .sessionWindow = clickHandlerContext.sessionWindow, }); } [[nodiscard]] Main::Session *ResolveMarkdownSession( const MarkdownMessageContext &context) { if (const auto controller = context.sessionWindow.get()) { return &controller->session(); } return nullptr; } [[nodiscard]] HistoryItem *ResolveMarkdownItem( const MarkdownMessageContext &context) { const auto session = ResolveMarkdownSession(context); const auto itemId = context.clickHandlerContext.itemId; return (session && itemId) ? session->data().message(itemId) : nullptr; } [[nodiscard]] bool CanShareMarkdownItem(not_null item) { const auto peer = item->history()->peer; return peer->allowsForwarding() && !item->forbidsForward(); } [[nodiscard]] QString RichMessageKey(FullMsgId itemId) { return u"rich-message:%1:%2"_q .arg(itemId.peer.value) .arg(itemId.msg.bare); } [[nodiscard]] uint64 RichMessagePageId(FullMsgId itemId) { return uint64(itemId.peer.value) ^ (uint64(itemId.msg.bare) << 1); } [[nodiscard]] bool PreparedContentHasAnchor( const Markdown::MarkdownArticleContent &content, const QString &anchorId) { if (anchorId.isEmpty()) { return false; } auto article = Markdown::MarkdownArticle(st::defaultMarkdown); article.setContent(content); const auto width = article.maxWidth(); static_cast(article.resizeGetHeight(width)); auto top = article.anchorTop(anchorId); if (top < 0) { const auto expansion = article.expandDetailsToAnchor(anchorId); if (expansion.changed) { static_cast(article.resizeGetHeight(width)); } top = article.anchorTop(anchorId); } return (top >= 0); } void OpenRichMessageChannel( not_null session, const QString &context) { const auto parsed = ParseNativeIvChannelContext(context); if (const auto channelId = ChannelId(parsed.channelId)) { const auto channel = session->data().channel(channelId); if (channel->isLoaded()) { if (const auto controller = session->tryResolveWindow(channel)) { controller->showPeerHistory(channel); } } else if (const auto username = ResolveNativeIvChannelUsername( channel->username(), parsed.username); !username.isEmpty()) { if (const auto controller = session->tryResolveWindow(channel)) { controller->showPeerByLink({ .usernameOrId = username, }); } } } } void JoinRichMessageChannel( not_null session, const QString &context) { const auto parsed = ParseNativeIvChannelContext(context); if (const auto channelId = ChannelId(parsed.channelId)) { const auto channel = session->data().channel(channelId); if (channel->isLoaded()) { session->api().joinChannel(channel); } else if (const auto username = ResolveNativeIvChannelUsername( channel->username(), parsed.username); !username.isEmpty()) { if (const auto controller = session->tryResolveWindow(channel)) { controller->showPeerByLink({ .usernameOrId = username, .joinChannel = true, }); } } } } [[nodiscard]] bool ActivateRichMessageMedia( const Markdown::MediaActivation &activation, Qt::MouseButton button, const QVariant &clickHandlerContext) { if (button != Qt::LeftButton && button != Qt::MiddleButton) { return false; } switch (activation.kind) { case Markdown::MediaActivationKind::None: return false; case Markdown::MediaActivationKind::ExternalUrl: if (activation.url.isEmpty()) { return false; } HiddenUrlClickHandler::Open(activation.url, clickHandlerContext); return true; case Markdown::MediaActivationKind::Embed: return false; case Markdown::MediaActivationKind::Photo: if (!activation.photo) { return false; } activation.photo->open(button); return true; case Markdown::MediaActivationKind::Document: if (!activation.document) { return false; } activation.document->open(button); return true; case Markdown::MediaActivationKind::OpenChannel: if (!activation.channel) { return false; } activation.channel->open(button); return true; case Markdown::MediaActivationKind::JoinChannel: if (!activation.channel) { return false; } activation.channel->join(button); return true; } return false; } [[nodiscard]] Markdown::OpenOptions PrepareLocalMarkdownOptions( QVariant context) { auto options = Markdown::OpenOptions{ .viewerKind = Markdown::ViewerKind::LocalFile, .clickHandlerContext = std::move(context), }; const auto messageContext = ExtractMarkdownMessageContext( options.clickHandlerContext); const auto item = messageContext ? ResolveMarkdownItem(*messageContext) : nullptr; if (item && CanShareMarkdownItem(not_null{ item })) { options.share = [context = *messageContext]( std::shared_ptr show) { const auto session = ResolveMarkdownSession(context); const auto itemId = context.clickHandlerContext.itemId; const auto current = (session && itemId) ? session->data().message(itemId) : nullptr; if (!show || !current || !CanShareMarkdownItem(not_null{ current })) { return; } FastShareMessage( Main::MakeSessionShow(show, not_null{ session }), not_null{ current }); }; } return options; } } // namespace class Shown final : public base::has_weak_ptr { public: Shown( not_null delegate, not_null session, not_null data, QString hash, Fn openChannel, Fn joinChannel); [[nodiscard]] bool showing( not_null session, not_null data) const; [[nodiscard]] bool showingFrom(not_null session) const; [[nodiscard]] bool activeFor(not_null session) const; [[nodiscard]] bool active() const; void moveTo(not_null data, QString hash); void update(not_null data); void showJoinedTooltip(); void minimize(); [[nodiscard]] rpl::producer events() const { return _events.events(); } [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } private: void prepare(not_null data, const QString &hash); void createMarkdownController( Markdown::MarkdownArticleContent content, QString title, QString initialFragment, not_null page); [[nodiscard]] Markdown::OpenOptions markdownOpenOptions( QString initialFragment, not_null page); void showMarkdownWindowed( Markdown::MarkdownArticleContent content, QString title, QString initialFragment, not_null page, bool refresh); [[nodiscard]] std::shared_ptr createMediaRuntime( not_null page); [[nodiscard]] bool activateMarkdownMedia( const Markdown::MediaActivation &activation, Qt::MouseButton button, const QVariant &clickHandlerContext) const; const not_null _delegate; const not_null _session; const Fn _openChannel; const Fn _joinChannel; QString _id; std::unique_ptr _markdownController; std::shared_ptr _markdownMediaRuntime; QString _markdownRuntimeUrl; rpl::event_stream _events; rpl::lifetime _lifetime; }; class TonSite final : public base::has_weak_ptr { public: TonSite(not_null delegate, QString uri); [[nodiscard]] bool active() const; void moveTo(QString uri); void minimize(); [[nodiscard]] rpl::producer events() const { return _events.events(); } [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } private: void createController(); void showWindowed(); const not_null _delegate; QString _uri; std::unique_ptr _controller; rpl::event_stream _events; rpl::lifetime _lifetime; }; Shown::Shown( not_null delegate, not_null session, not_null data, QString hash, Fn openChannel, Fn joinChannel) : _delegate(delegate) , _session(session) , _openChannel(std::move(openChannel)) , _joinChannel(std::move(joinChannel)) { prepare(data, hash); } void Shown::prepare(not_null data, const QString &hash) { const auto richPage = data->richPage(); const auto id = data->id(); const auto page = _session->data().webpage(data->pageId()); if (_markdownRuntimeUrl != id) { _markdownMediaRuntime = nullptr; _markdownRuntimeUrl = QString(); } _id = id; auto prepared = Markdown::TryPrepareNativeInstantView({ .richPage = richPage, .mediaRuntime = createMediaRuntime(page), }); showMarkdownWindowed( std::move(prepared.content), data->name(), hash, page, false); } Markdown::OpenOptions Shown::markdownOpenOptions( QString initialFragment, not_null page) { const auto clickHandlerContext = std::make_shared(); auto options = Markdown::OpenOptions{ .sourceName = page->displayedSiteName(), .sourceUrl = page->url, .initialFragment = std::move(initialFragment), .currentPageId = page->id, .viewerKind = Markdown::ViewerKind::InstantView, .clickHandlerContextRef = clickHandlerContext, .ivWebviewStorageId = _session->local().resolveStorageIdOther(), .activateMedia = [=]( const Markdown::MediaActivation &activation, Qt::MouseButton button) { return activateMarkdownMedia(activation, button, *clickHandlerContext); }, .downloadTaskFinished = page->session().downloaderTaskFinished(), }; if (!page->url.isEmpty()) { options.share = [=, url = page->url](std::shared_ptr show) { if (!show) { return; } FastShareLink(Main::MakeSessionShow(show, _session), url); }; } return options; } void Shown::createMarkdownController( Markdown::MarkdownArticleContent content, QString title, QString initialFragment, not_null page) { Expects(!_markdownController); auto options = markdownOpenOptions(std::move(initialFragment), page); _markdownController = std::make_unique( _delegate, std::move(content), std::move(title), nullptr, std::move(options)); _markdownController->events() | rpl::on_next([=](Markdown::Event event) { using FromType = Markdown::Event::Type; using ToType = Controller::Event::Type; switch (event.type) { case FromType::Close: _events.fire({ .type = ToType::Close }); break; case FromType::Quit: _events.fire({ .type = ToType::Quit }); break; case FromType::OpenPage: _events.fire({ .type = ToType::OpenPage, .url = event.url, .context = QString::number(event.webpageId), .webpageId = event.webpageId, }); break; case FromType::OpenFile: break; case FromType::Report: _events.fire({ .type = ToType::Report, .context = QString::number(event.webpageId), }); break; } }, _markdownController->lifetime()); } void Shown::showMarkdownWindowed( Markdown::MarkdownArticleContent content, QString title, QString initialFragment, not_null page, bool refresh) { if (!_markdownController) { createMarkdownController( std::move(content), std::move(title), std::move(initialFragment), page); _markdownController->activate(); return; } auto options = markdownOpenOptions(std::move(initialFragment), page); if (refresh) { _markdownController->update( std::move(content), std::move(title), std::move(options)); } else { _markdownController->show( std::move(content), std::move(title), std::move(options)); } } std::shared_ptr Shown::createMediaRuntime( not_null page) { if (_markdownMediaRuntime && (page->url == _markdownRuntimeUrl)) { return _markdownMediaRuntime; } _markdownRuntimeUrl = page->url; _markdownMediaRuntime = CreateCachedPageMediaRuntime( _session, page, _openChannel, _joinChannel); return _markdownMediaRuntime; } bool Shown::activateMarkdownMedia( const Markdown::MediaActivation &activation, Qt::MouseButton button, const QVariant &clickHandlerContext) const { if (button != Qt::LeftButton && button != Qt::MiddleButton) { return false; } switch (activation.kind) { case Markdown::MediaActivationKind::None: return false; case Markdown::MediaActivationKind::ExternalUrl: if (activation.url.isEmpty()) { return false; } HiddenUrlClickHandler::Open(activation.url, clickHandlerContext); return true; case Markdown::MediaActivationKind::Embed: return false; case Markdown::MediaActivationKind::Photo: if (!activation.photo) { return false; } activation.photo->open(button); return true; case Markdown::MediaActivationKind::Document: if (!activation.document) { return false; } activation.document->open(button); return true; case Markdown::MediaActivationKind::OpenChannel: if (!activation.channel) { return false; } activation.channel->open(button); return true; case Markdown::MediaActivationKind::JoinChannel: if (!activation.channel) { return false; } activation.channel->join(button); return true; } return false; } bool Shown::showing( not_null session, not_null data) const { return showingFrom(session) && (_id == data->id()); } bool Shown::showingFrom(not_null session) const { return (_session == session); } bool Shown::activeFor(not_null session) const { return showingFrom(session) && _markdownController; } bool Shown::active() const { return _markdownController && _markdownController->active(); } void Shown::moveTo(not_null data, QString hash) { prepare(data, hash); } void Shown::update(not_null data) { const auto richPage = data->richPage(); const auto id = data->id(); if (_markdownRuntimeUrl != id) { _markdownMediaRuntime = nullptr; _markdownRuntimeUrl = QString(); } _id = id; const auto page = _session->data().webpage(data->pageId()); auto prepared = Markdown::TryPrepareNativeInstantView({ .richPage = richPage, .mediaRuntime = createMediaRuntime(page), }); showMarkdownWindowed( std::move(prepared.content), data->name(), QString(), page, true); } void Shown::showJoinedTooltip() { if (_markdownController) { _markdownController->showJoinedTooltip(); } } void Shown::minimize() { if (_markdownController) { _markdownController->minimize(); } } TonSite::TonSite(not_null delegate, QString uri) : _delegate(delegate) , _uri(uri) { showWindowed(); } void TonSite::createController() { Expects(!_controller); _controller = std::make_unique(_delegate); _controller->events( ) | rpl::start_to_stream(_events, _controller->lifetime()); } void TonSite::showWindowed() { if (!_controller) { createController(); } _controller->showTonSite(Storage::TonSiteStorageId(), _uri); } bool TonSite::active() const { return _controller && _controller->active(); } void TonSite::moveTo(QString uri) { _controller->showTonSite({}, uri); } void TonSite::minimize() { if (_controller) { _controller->minimize(); } } Instance::Instance(not_null delegate) : _delegate(delegate) { } Instance::~Instance() = default; void Instance::show( not_null controller, not_null data, QString hash) { showOpenedPage(&controller->session(), data, std::move(hash), true); } void Instance::show( std::shared_ptr show, not_null data, QString hash) { showOpenedPage(&show->session(), data, std::move(hash), true); } void Instance::show( not_null session, not_null data, QString hash) { showOpenedPage(session, data, std::move(hash), true); } void Instance::showOpenedPage( not_null session, not_null data, QString hash, bool requestFullOnOpen) { if (Platform::IsMac()) { // Otherwise IV is not visible under the media viewer. Core::App().hideMediaView(); } if (Core::App().settings().normalizeIvZoom()) { Core::App().saveSettingsDelayed(); } primeFullRequest(session, data); const auto guard = gsl::finally([&] { if (requestFullOnOpen) { requestFull(session, data->id()); } }); if (_shown && _shownSession == session) { _shown->moveTo(data, hash); return; } _shown = std::make_unique( _delegate, session, data, hash, [=](QString context) { processOpenChannel(context); }, [=](QString context) { processJoinChannel(context); }); _shownSession = session; _shown->events() | rpl::on_next([=](Controller::Event event) { using Type = Controller::Event::Type; const auto lower = event.url.toLower(); const auto urlChecked = lower.startsWith("http://") || lower.startsWith("https://"); const auto tonsite = lower.startsWith("tonsite://"); switch (event.type) { case Type::Close: _shown = nullptr; break; case Type::Quit: Shortcuts::Launch(Shortcuts::Command::Quit); break; case Type::OpenChannel: processOpenChannel(event.context); break; case Type::JoinChannel: processJoinChannel(event.context); break; case Type::OpenLinkExternal: if (urlChecked) { File::OpenUrl(event.url); closeAll(); } else if (tonsite) { showTonSite(event.url); } break; case Type::OpenMedia: if (const auto window = Core::App().activeWindow()) { const auto current = window->sessionController(); const auto controller = (current && ¤t->session() == _shownSession) ? current : nullptr; const auto item = (HistoryItem*)nullptr; const auto topicRootId = MsgId(0); const auto monoforumPeerId = PeerId(0); if (event.context.startsWith("-photo")) { const auto id = event.context.mid(6).toULongLong(); const auto photo = _shownSession->data().photo(id); if (!photo->isNull()) { window->openInMediaView({ controller, photo, item, topicRootId, monoforumPeerId }); } } else if (event.context.startsWith("-video")) { const auto id = event.context.mid(6).toULongLong(); const auto video = _shownSession->data().document(id); if (!video->isNull()) { window->openInMediaView({ controller, video, item, topicRootId, monoforumPeerId }); } } } break; case Type::OpenPage: case Type::OpenLink: { if (tonsite) { showTonSite(event.url); break; } else if (!urlChecked) { break; } const auto session = _shownSession; const auto url = event.url; const auto parts = event.url.split('#'); const auto hash = (parts.size() > 1) ? parts[1] : u""_q; if (event.webpageId) { const auto page = session->data().webpage( WebPageId(event.webpageId)).get(); if (page->iv) { this->showOpenedPage(session, page->iv.get(), hash, false); break; } } const auto requestKey = event.webpageId ? QString::number(event.webpageId) : url; auto &requested = _fullRequested[session][requestKey]; if (event.webpageId) { const auto page = session->data().webpage( WebPageId(event.webpageId)).get(); if (page->iv) { requested.page = page; requested.hash = page->iv->hash(); } else { requested.hash = 0; } } requested.lastRequestedAt = crl::now(); session->api().request(MTPmessages_GetWebPage( MTP_string(url), MTP_int(requested.hash) )).done([=](const MTPmessages_WebPage &result) { const auto processed = processReceivedPage( session, requestKey, result); if (const auto page = processed.page; page && page->iv) { if (event.webpageId && page->id != event.webpageId) { const auto expected = session->data().webpage( WebPageId(event.webpageId)).get(); if (expected->iv) { this->showOpenedPage( session, expected->iv.get(), hash, false); } else { UrlClickHandler::Open(event.url); } return; } this->showOpenedPage(session, page->iv.get(), hash, false); } else { UrlClickHandler::Open(event.url); } }).fail([=] { UrlClickHandler::Open(event.url); }).send(); } break; case Type::Report: if (const auto controller = _shownSession->tryResolveWindow()) { controller->window().activate(); controller->showPeerByLink(Window::PeerByLinkInfo{ .usernameOrId = "previews", .resolveType = Window::ResolveType::BotStart, .startToken = ("webpage" + QString::number(event.context.toULongLong())), }); } break; } }, _shown->lifetime()); session->changes().peerUpdates( ::Data::PeerUpdate::Flag::ChannelAmIn ) | rpl::on_next([=](const ::Data::PeerUpdate &update) { if (const auto channel = update.peer->asChannel()) { if (channel->amIn()) { const auto i = _joining.find(session); const auto value = not_null{ channel }; if (i != end(_joining) && i->second.remove(value)) { _shown->showJoinedTooltip(); } } } }, _shown->lifetime()); trackSession(session); } void Instance::primeFullRequest( not_null session, not_null data) { auto &requested = _fullRequested[session][data->id()]; requested.page = session->data().webpage(data->pageId()).get(); requested.hash = data->hash(); } void Instance::trackSession(not_null session) { if (!_tracking.emplace(session).second) { return; } session->data().sessionDataAboutToBeCleared( ) | rpl::on_next([=] { closeSessionDataViews(session); }, session->lifetime()); session->data().itemRemoved( ) | rpl::on_next([=](not_null item) { closeMarkdownsForItem(session, item->fullId()); }, session->lifetime()); session->data().itemsAboutToBeDestroyed( ) | rpl::on_next([=](std::vector> items) { for (const auto &item : items) { closeMarkdownsForItem(session, item->fullId()); } }, session->lifetime()); session->lifetime().add([=] { closeSessionDataViews(session); _tracking.remove(session); _joining.remove(session); _fullRequested.remove(session); if (const auto i = _richMessageRequested.find(session) ; i != end(_richMessageRequested)) { for (const auto &[itemId, requested] : i->second) { if (requested.requestId) { session->api().request(requested.requestId).cancel(); } } _richMessageRequested.erase(i); } _ivCache.remove(session); if (_ivRequestSession == session) { session->api().request(_ivRequestId).cancel(); _ivRequestSession = nullptr; _ivRequestUri = QString(); _ivRequestId = 0; } }); } void Instance::bindMarkdown( const QString &key, not_null session, FullMsgId itemId) { _markdownBindings[key] = { .session = session.get(), .itemId = itemId, }; trackSession(session); } void Instance::closeMarkdownsForItem( not_null session, FullMsgId itemId) { if (!itemId) { return; } auto keys = std::vector(); for (const auto &[key, binding] : _markdownBindings) { if (binding.session == session.get() && binding.itemId == itemId) { keys.push_back(key); } } for (const auto &key : keys) { _markdownBindings.remove(key); _markdowns.take(key); } } void Instance::closeMarkdownsForSession(not_null session) { auto keys = std::vector(); for (const auto &[key, binding] : _markdownBindings) { if (binding.session == session.get()) { keys.push_back(key); } } for (const auto &key : keys) { _markdownBindings.remove(key); _markdowns.take(key); } } void Instance::closeSessionDataViews(not_null session) { closeMarkdownsForSession(session); if (_shownSession == session) { _shownSession = nullptr; } if (_shown && _shown->showingFrom(session)) { _shown = nullptr; } } void Instance::openWithIvPreferred( not_null controller, QString uri, QVariant context) { auto my = context.value(); my.sessionWindow = controller; openWithIvPreferred( &controller->session(), uri, QVariant::fromValue(my)); } void Instance::openWithIvPreferred( not_null session, QString uri, QVariant context) { const auto openExternal = [=] { auto my = context.value(); my.ignoreIv = true; const auto updated = QVariant::fromValue(my); if (my.forceExternalUrlConfirmation) { HiddenUrlClickHandler::Open(uri, updated); } else { UrlClickHandler::Open(uri, updated); } }; const auto parts = uri.split('#'); if (parts.isEmpty() || parts[0].isEmpty()) { return; } trackSession(session); const auto hash = (parts.size() > 1) ? parts[1] : u""_q; const auto url = parts[0]; auto &cache = _ivCache[session]; if (const auto i = cache.find(url); i != end(cache)) { const auto page = i->second; if (page && page->iv) { auto my = context.value(); if (const auto window = my.sessionWindow.get()) { show(window, page->iv.get(), hash); } else { show(session, page->iv.get(), hash); } } else { openExternal(); } return; } else if (_ivRequestSession == session.get() && _ivRequestUri == uri) { return; } else if (_ivRequestId) { _ivRequestSession->api().request(_ivRequestId).cancel(); } const auto finish = [=](WebPageData *page) { Expects(_ivRequestSession == session); _ivRequestId = 0; _ivRequestUri = QString(); _ivRequestSession = nullptr; _ivCache[session][url] = page; if (page && page->iv) { this->showOpenedPage(session, page->iv.get(), hash, false); } else { openExternal(); } }; _ivRequestSession = session; _ivRequestUri = uri; auto &requested = _fullRequested[session][url]; requested.lastRequestedAt = crl::now(); _ivRequestId = session->api().request(MTPmessages_GetWebPage( MTP_string(url), MTP_int(requested.hash) )).done([=](const MTPmessages_WebPage &result) { finish(processReceivedPage(session, url, result).page); }).fail([=] { finish(nullptr); }).send(); } void Instance::showTonSite( const QString &uri, QVariant context) { if (!Controller::IsGoodTonSiteUrl(uri)) { Ui::Toast::Show(tr::lng_iv_not_supported(tr::now)); return; } else if (Platform::IsMac()) { // Otherwise IV is not visible under the media viewer. Core::App().hideMediaView(); } if (_tonSite) { _tonSite->moveTo(uri); return; } _tonSite = std::make_unique(_delegate, uri); _tonSite->events() | rpl::on_next([=](Controller::Event event) { using Type = Controller::Event::Type; const auto lower = event.url.toLower(); const auto urlChecked = lower.startsWith("http://") || lower.startsWith("https://"); const auto tonsite = lower.startsWith("tonsite://"); switch (event.type) { case Type::Close: _tonSite = nullptr; break; case Type::Quit: Shortcuts::Launch(Shortcuts::Command::Quit); break; case Type::OpenLinkExternal: if (urlChecked) { File::OpenUrl(event.url); closeAll(); } else if (tonsite) { showTonSite(event.url); } break; case Type::OpenPage: case Type::OpenLink: if (urlChecked) { UrlClickHandler::Open(event.url); } else if (tonsite) { showTonSite(event.url); } break; } }, _tonSite->lifetime()); } Instance::RichMessageGeneration Instance::CaptureRichMessageGeneration( not_null item) { auto generation = RichMessageGeneration(); generation.inlinePage = item->richPage(); generation.fullPage = item->fullRichPage(); generation.fullPageVersion = item->fullRichPageVersion(); return generation; } bool Instance::MatchesRichMessageGeneration( not_null item, const RichMessageGeneration &generation) { return (item->richPage() == generation.inlinePage) && (item->fullRichPage() == generation.fullPage) && (item->fullRichPageVersion() == generation.fullPageVersion); } void Instance::resolveRichMessage( not_null controller, not_null item, RichMessageResolved done) { if (const auto page = item->fullRichPage()) { done(page); return; } const auto richPage = item->richPage(); if (richPage && !richPage->part) { done(richPage); return; } const auto session = &controller->session(); const auto itemId = item->fullId(); const auto generation = CaptureRichMessageGeneration(item); trackSession(session); auto &requested = _richMessageRequested[session][itemId]; if (requested.requestId) { if (MatchesRichMessageGeneration(item, requested.generation)) { requested.callbacks.push_back(std::move(done)); return; } session->api().request(requested.requestId).cancel(); requested = RichMessageRequest(); } requested.callbacks.push_back(std::move(done)); const auto now = crl::now(); if (requested.lastRequestedAt && MatchesRichMessageGeneration(item, requested.generation) && (now - requested.lastRequestedAt) < kAllowPageReloadAfter) { finishRichMessageRequest(session, itemId, requested.token, nullptr); return; } requested.lastRequestedAt = now; requested.generation = generation; requested.token = ++_nextRichMessageRequestToken; const auto token = requested.token; requested.requestId = session->api().request(MTPmessages_GetRichMessage( item->history()->peer->input(), MTP_int(item->id) )).done([=](const MTPmessages_Messages &result) { const auto processed = processReceivedRichMessage( session, itemId, generation, result); finishRichMessageRequest( session, itemId, token, processed.page, processed.notifyCallbacks); }).fail([=] { finishRichMessageRequest( session, itemId, token, nullptr); }).send(); } void Instance::showRichMessage( not_null controller, not_null item, QString initialFragment) { const auto weak = base::make_weak(controller); const auto itemId = item->fullId(); resolveRichMessage(controller, item, [=](std::shared_ptr page) { const auto strong = weak.get(); const auto current = strong ? strong->session().data().message(itemId) : nullptr; if (!page || !current) { if (strong && !page) { Ui::Toast::Show(tr::lng_iv_not_supported(tr::now)); } return; } showRichMessage( not_null{ strong }, not_null{ current }, std::move(page), initialFragment); }); } void Instance::showRichMessage( not_null controller, not_null item, std::shared_ptr richPage, QString initialFragment) { if (Platform::IsMac()) { Core::App().hideMediaView(); } const auto session = &controller->session(); const auto itemId = item->fullId(); const auto key = RichMessageKey(itemId); const auto title = item->history()->peer->name(); auto clickHandlerContext = ClickHandlerContext(); clickHandlerContext.sessionWindow = controller; clickHandlerContext.itemId = itemId; auto context = QVariant::fromValue(clickHandlerContext); auto mediaRuntime = CreateMessageMediaRuntime( session, itemId, [=](QString context) { OpenRichMessageChannel(session, context); }, [=](QString context) { JoinRichMessageChannel(session, context); }); const auto richLimits = ResolveRichMessageLimits(session); auto prepared = Markdown::TryPrepareNativeInstantView({ .richPage = richPage, .mediaRuntime = std::move(mediaRuntime), .tableRenderLimits = Markdown::PrepareTableRenderLimitsForRichMessage( richLimits), }); if (!prepared.supported()) { Ui::Toast::Show(tr::lng_iv_not_supported(tr::now)); return; } if (!initialFragment.isEmpty() && !PreparedContentHasAnchor(prepared.content, initialFragment)) { controller->showToast(tr::lng_iv_not_found_in_message(tr::now)); return; } auto options = Markdown::OpenOptions{ .sourceName = title, .currentPageId = RichMessagePageId(itemId), .viewerKind = Markdown::ViewerKind::InstantView, .clickHandlerContext = context, .activateMedia = [=]( const Markdown::MediaActivation &activation, Qt::MouseButton button) { return ActivateRichMessageMedia(activation, button, context); }, .downloadTaskFinished = session->downloaderTaskFinished(), }; options.initialFragment = std::move(initialFragment); if (CanShareMarkdownItem(item)) { options.share = [=](std::shared_ptr show) { const auto current = session->data().message(itemId); if (!show || !current || !CanShareMarkdownItem(not_null{ current })) { return; } FastShareMessage( Main::MakeSessionShow(show, not_null{ session }), not_null{ current }); }; } auto i = _markdowns.find(key); if (i == end(_markdowns)) { auto controller = std::make_unique( _delegate, std::move(prepared.content), title, nullptr, std::move(options)); controller->events() | rpl::on_next([=](Markdown::Event event) { using Type = Markdown::Event::Type; switch (event.type) { case Type::Close: _markdownBindings.remove(key); _markdowns.take(key); break; case Type::Quit: Shortcuts::Launch(Shortcuts::Command::Quit); break; case Type::OpenPage: case Type::OpenFile: if (!event.url.isEmpty()) { UrlClickHandler::Open(event.url, event.context); } break; case Type::Report: break; } }, controller->lifetime()); i = _markdowns.emplace(key, std::move(controller)).first; } else { i->second->show( std::move(prepared.content), title, std::move(options)); } bindMarkdown(key, session, itemId); i->second->activate(); } bool Instance::showMarkdown( const QString &path, QVariant context) { const auto target = ParseLocalMarkdownTarget(path); const auto messageContext = ExtractMarkdownMessageContext(context); const auto session = messageContext ? ResolveMarkdownSession(*messageContext) : nullptr; const auto itemId = messageContext ? messageContext->clickHandlerContext.itemId : FullMsgId(); auto options = PrepareLocalMarkdownOptions(context); if (!target.sourceName.isEmpty()) { options.sourceName = target.sourceName; options.sourcePath = target.key; } options.initialFragment = target.fragment; auto i = _markdowns.find(target.key); if (i == end(_markdowns)) { if (auto controller = Markdown::TryOpenLocalFile( _delegate, target.path, std::move(options))) { controller->events() | rpl::on_next([=](Markdown::Event event) { using Type = Markdown::Event::Type; switch (event.type) { case Type::Close: _markdownBindings.remove(target.key); _markdowns.take(target.key); break; case Type::Quit: Shortcuts::Launch(Shortcuts::Command::Quit); break; case Type::Report: break; // Don't try opening markdown links inside markdown viewer, // messenger-provided markdown files should know nothing // about other local files and their paths. // //case Type::OpenFile: // if (!showMarkdown(event.url, event.context)) { // DEBUG_LOG(("Native Markdown IV: " // "failed local markdown link: %1" // ).arg(event.url)); // } // break; } }, controller->lifetime()); i = _markdowns.emplace(target.key, std::move(controller)).first; } else { return false; } } else { i->second->updateOptions(std::move(options)); } if (session) { bindMarkdown(target.key, not_null{ session }, itemId); } else { _markdownBindings.remove(target.key); } i->second->activate(); return true; } void Instance::requestFull( not_null session, const QString &id) { if (!_tracking.contains(session)) { return; } auto &requested = _fullRequested[session][id]; const auto last = requested.lastRequestedAt; const auto now = crl::now(); if (last && (now - last) < kAllowPageReloadAfter) { return; } requested.lastRequestedAt = now; session->api().request(MTPmessages_GetWebPage( MTP_string(id), MTP_int(requested.hash) )).done([=](const MTPmessages_WebPage &result) { const auto processed = processReceivedPage(session, id, result); if (processed.articleChanged && processed.page && processed.page->iv && _shown && _shownSession == session) { _shown->update(processed.page->iv.get()); } }).send(); } Instance::ProcessReceivedPageResult Instance::processReceivedPage( not_null session, const QString &url, const MTPmessages_WebPage &result) { const auto &data = result.data(); const auto owner = &session->data(); owner->processUsers(data.vusers()); owner->processChats(data.vchats()); auto &requested = _fullRequested[session][url]; auto processed = ProcessReceivedPageResult(); const auto &mtp = data.vwebpage(); mtp.match([&](const MTPDwebPageNotModified &data) { Q_UNUSED(data); processed.page = requested.page; }, [&](const MTPDwebPage &data) { const auto oldPage = requested.page; const auto oldVersion = oldPage ? oldPage->version : 0; const auto oldIvHash = (oldPage && oldPage->iv) ? oldPage->iv->hash() : 0; requested.hash = data.vhash().v; processed.page = owner->processWebpage(data).get(); requested.page = processed.page; processed.articleChanged = processed.page && processed.page->iv && (!oldPage || processed.page->version != oldVersion || processed.page->iv->hash() != oldIvHash); }, [&](const auto &) { processed.page = owner->processWebpage(mtp).get(); requested.page = processed.page; }); return processed; } auto Instance::processReceivedRichMessage( not_null session, FullMsgId itemId, const RichMessageGeneration &generation, const MTPmessages_Messages &result) -> ProcessReceivedRichMessageResult { const auto owner = &session->data(); auto processed = ProcessReceivedRichMessageResult(); auto page = std::shared_ptr(); result.match([&](const MTPDmessages_messagesNotModified &) { LOG(("API Error: received messages.messagesNotModified!")); }, [&](const auto &data) { owner->processUsers(data.vusers()); owner->processChats(data.vchats()); for (const auto &message : data.vmessages().v) { if (message.type() != mtpc_message) { continue; } const auto &parsed = message.c_message(); if (MsgId(parsed.vid().v) != itemId.msg) { continue; } const auto richMessage = parsed.vrich_message(); page = richMessage ? ParseRichPage(session, *richMessage) : nullptr; break; } }); const auto peer = owner->peer(itemId.peer); result.match([&](const MTPDmessages_channelMessages &data) { if (const auto channel = peer->asChannel()) { channel->ptsReceived(data.vpts().v); } else { LOG(("App Error: received messages.channelMessages!")); } }, [](const auto &) {}); result.match([&](const MTPDmessages_messagesNotModified &) { }, [&](const auto &data) { peer->processTopics(data.vtopics()); }); const auto current = owner->message(itemId); if (!current || !MatchesRichMessageGeneration(not_null{ current }, generation)) { return processed; } if (page) { current->setFullRichPage(page); } processed.page = std::move(page); return processed; } void Instance::finishRichMessageRequest( not_null session, FullMsgId itemId, uint64 token, std::shared_ptr page, bool notifyCallbacks) { const auto sessionIt = _richMessageRequested.find(session); if (sessionIt == end(_richMessageRequested)) { return; } const auto itemIt = sessionIt->second.find(itemId); if (itemIt == end(sessionIt->second)) { return; } if (itemIt->second.token != token) { return; } auto callbacks = std::move(itemIt->second.callbacks); itemIt->second.callbacks.clear(); itemIt->second.requestId = 0; if (!notifyCallbacks) { return; } for (auto &callback : callbacks) { callback(page); } } void Instance::processOpenChannel(const QString &context) { if (!_shownSession) { return; } const auto parsed = ParseNativeIvChannelContext(context); if (const auto channelId = ChannelId(parsed.channelId)) { const auto channel = _shownSession->data().channel(channelId); if (channel->isLoaded()) { if (const auto controller = _shownSession->tryResolveWindow(channel)) { controller->showPeerHistory(channel); _shown = nullptr; } } else if (const auto username = ResolveNativeIvChannelUsername( channel->username(), parsed.username); !username.isEmpty()) { if (const auto controller = _shownSession->tryResolveWindow(channel)) { controller->showPeerByLink({ .usernameOrId = username, }); _shown = nullptr; } } } } void Instance::processJoinChannel(const QString &context) { if (!_shownSession) { return; } const auto parsed = ParseNativeIvChannelContext(context); if (const auto channelId = ChannelId(parsed.channelId)) { const auto channel = _shownSession->data().channel(channelId); _joining[_shownSession].emplace(channel); if (channel->isLoaded()) { _shownSession->api().joinChannel(channel); } else if (const auto username = ResolveNativeIvChannelUsername( channel->username(), parsed.username); !username.isEmpty()) { if (const auto controller = _shownSession->tryResolveWindow(channel)) { controller->showPeerByLink({ .usernameOrId = username, .joinChannel = true, }); } } } } bool Instance::hasActiveWindow(not_null session) const { return _shown && _shown->activeFor(session); } bool Instance::closeActive() { if (_shown && _shown->active()) { _shown = nullptr; return true; } else if (_tonSite && _tonSite->active()) { _tonSite = nullptr; return true; } for (auto &[key, controller] : _markdowns) { if (controller->active()) { _markdowns.take(key); return true; } } return false; } bool Instance::minimizeActive() { if (_shown && _shown->active()) { _shown->minimize(); return true; } else if (_tonSite && _tonSite->active()) { _tonSite->minimize(); return true; } return false; } void Instance::closeAll() { _shown = nullptr; _tonSite = nullptr; } bool PreferForUri(const QString &uri) { const auto url = QUrl(uri); const auto host = url.host().toLower(); const auto path = url.path().toLower(); return (host == u"telegra.ph"_q) || (host == u"te.legra.ph"_q) || (host == u"graph.org"_q) || (host == u"telegram.org"_q && (path.startsWith(u"/faq"_q) || path.startsWith(u"/privacy"_q) || path.startsWith(u"/blog"_q))); } } // namespace Iv