/* 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 "core/local_url_handlers.h" #include "api/api_text_entities.h" #include "api/api_chat_invite.h" #include "base/qthelp_regex.h" #include "base/qthelp_url.h" #include "lang/lang_cloud_manager.h" #include "lang/lang_keys.h" #include "core/update_checker.h" #include "core/application.h" #include "boxes/confirm_phone_box.h" #include "boxes/background_preview_box.h" #include "boxes/confirm_box.h" #include "boxes/share_box.h" #include "boxes/connection_box.h" #include "boxes/sticker_set_box.h" #include "boxes/sessions_box.h" #include "boxes/language_box.h" #include "passport/passport_form_controller.h" #include "window/window_session_controller.h" #include "data/data_session.h" #include "data/data_document.h" #include "data/data_cloud_themes.h" #include "data/data_channel.h" #include "media/player/media_player_instance.h" #include "window/window_session_controller.h" #include "window/window_controller.h" #include "settings/settings_common.h" #include "mainwidget.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "apiwrap.h" #include "app.h" namespace Core { namespace { using Match = qthelp::RegularExpressionMatch; bool JoinGroupByHash( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } Api::CheckChatInvite(controller, match->captured(1)); return true; } bool ShowStickerSet( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } Core::App().hideMediaView(); Ui::show(Box( controller, MTP_inputStickerSetShortName(MTP_string(match->captured(1))))); return true; } bool ShowTheme( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto clickFromMessageId = context.value(); Core::App().hideMediaView(); controller->session().data().cloudThemes().resolve( match->captured(1), clickFromMessageId); return true; } void ShowLanguagesBox() { static auto Guard = base::binary_guard(); Guard = LanguageBox::Show(); } bool SetLanguage( Window::SessionController *controller, const Match &match, const QVariant &context) { if (match->capturedRef(1).isEmpty()) { ShowLanguagesBox(); } else { const auto languageId = match->captured(2); Lang::CurrentCloudManager().switchWithWarning(languageId); } return true; } bool ShareUrl( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); auto url = params.value(qsl("url")); if (url.isEmpty()) { return false; } else { controller->content()->shareUrlLayer(url, params.value("text")); return true; } return false; } bool ConfirmPhone( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); auto phone = params.value(qsl("phone")); auto hash = params.value(qsl("hash")); if (phone.isEmpty() || hash.isEmpty()) { return false; } ConfirmPhoneBox::Start(&controller->session(), phone, hash); return true; } bool ShareGameScore( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); ShareGameScoreByHash(&controller->session(), params.value(qsl("hash"))); return true; } bool ApplySocksProxy( Window::SessionController *controller, const Match &match, const QVariant &context) { auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); ProxiesBoxController::ShowApplyConfirmation( MTP::ProxyData::Type::Socks5, params); return true; } bool ApplyMtprotoProxy( Window::SessionController *controller, const Match &match, const QVariant &context) { auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); ProxiesBoxController::ShowApplyConfirmation( MTP::ProxyData::Type::Mtproto, params); return true; } bool ShowPassportForm( Window::SessionController *controller, const QMap ¶ms) { if (!controller) { return false; } const auto botId = params.value("bot_id", QString()).toInt(); const auto scope = params.value("scope", QString()); const auto callback = params.value("callback_url", QString()); const auto publicKey = params.value("public_key", QString()); const auto nonce = params.value( Passport::NonceNameByScope(scope), QString()); const auto errors = params.value("errors", QString()); controller->showPassportForm(Passport::FormRequest( botId, scope, callback, publicKey, nonce, errors)); return true; } bool ShowPassport( Window::SessionController *controller, const Match &match, const QVariant &context) { return ShowPassportForm( controller, url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower)); } bool ShowWallPaper( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); return BackgroundPreviewBox::Start( controller, params.value(qsl("slug")), params); } bool ResolveUsername( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); const auto domain = params.value(qsl("domain")); const auto valid = [](const QString &domain) { return qthelp::regex_match( qsl("^[a-zA-Z0-9\\.\\_]+$"), domain, {} ).valid(); }; if (domain == qsl("telegrampassport")) { return ShowPassportForm(controller, params); } else if (!valid(domain)) { return false; } auto start = qsl("start"); auto startToken = params.value(start); if (startToken.isEmpty()) { start = qsl("startgroup"); startToken = params.value(start); if (startToken.isEmpty()) { start = QString(); } } auto post = (start == qsl("startgroup")) ? ShowAtProfileMsgId : ShowAtUnreadMsgId; const auto postParam = params.value(qsl("post")); if (const auto postId = postParam.toInt()) { post = postId; } const auto commentParam = params.value(qsl("comment")); const auto commentId = commentParam.toInt(); const auto threadParam = params.value(qsl("thread")); const auto threadId = threadParam.toInt(); const auto gameParam = params.value(qsl("game")); if (!gameParam.isEmpty() && valid(gameParam)) { startToken = gameParam; post = ShowAtGameShareMsgId; } const auto clickFromMessageId = context.value(); using Navigation = Window::SessionNavigation; controller->showPeerByLink(Navigation::PeerByLinkInfo{ .usernameOrId = domain, .messageId = post, .repliesInfo = commentId ? Navigation::RepliesByLinkInfo{ Navigation::CommentId{ commentId } } : threadId ? Navigation::RepliesByLinkInfo{ Navigation::ThreadId{ threadId } } : Navigation::RepliesByLinkInfo{ v::null }, .startToken = startToken, .clickFromMessageId = clickFromMessageId, }); return true; } bool ResolvePrivatePost( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); const auto channelId = params.value(qsl("channel")).toInt(); const auto msgId = params.value(qsl("post")).toInt(); const auto commentParam = params.value(qsl("comment")); const auto commentId = commentParam.toInt(); const auto threadParam = params.value(qsl("thread")); const auto threadId = threadParam.toInt(); if (!channelId || !IsServerMsgId(msgId)) { return false; } const auto clickFromMessageId = context.value(); using Navigation = Window::SessionNavigation; controller->showPeerByLink(Navigation::PeerByLinkInfo{ .usernameOrId = channelId, .messageId = msgId, .repliesInfo = commentId ? Navigation::RepliesByLinkInfo{ Navigation::CommentId{ commentId } } : threadId ? Navigation::RepliesByLinkInfo{ Navigation::ThreadId{ threadId } } : Navigation::RepliesByLinkInfo{ v::null }, .clickFromMessageId = clickFromMessageId, }); return true; } bool ResolveSettings( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto section = match->captured(1).mid(1).toLower(); if (section.isEmpty()) { controller->window().showSettings(); return true; } if (section == qstr("devices")) { Ui::show(Box(&controller->session())); return true; } else if (section == qstr("language")) { ShowLanguagesBox(); return true; } const auto type = (section == qstr("folders")) ? ::Settings::Type::Folders : ::Settings::Type::Main; controller->showSettings(type); return true; } bool HandleUnknown( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto request = match->captured(1); const auto callback = crl::guard(controller, [=](const MTPDhelp_deepLinkInfo &result) { const auto text = TextWithEntities{ qs(result.vmessage()), Api::EntitiesFromMTP( &controller->session(), result.ventities().value_or_empty()) }; if (result.is_update_app()) { const auto callback = [=](Fn &&close) { Core::UpdateApplication(); close(); }; Ui::show(Box( text, tr::lng_menu_update(tr::now), callback)); } else { Ui::show(Box(text)); } }); controller->session().api().requestDeepLinkInfo(request, callback); return true; } bool OpenMediaTimestamp( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto time = match->captured(2).toInt(); if (time < 0) { return false; } const auto base = match->captured(1); if (base.startsWith(qstr("doc"))) { const auto parts = base.mid(3).split('_'); const auto documentId = parts.value(0).toULongLong(); const auto itemId = FullMsgId( parts.value(1).toInt(), parts.value(2).toInt()); const auto session = &controller->session(); const auto document = session->data().document(documentId); session->settings().setMediaLastPlaybackPosition( documentId, time * crl::time(1000)); if (document->isVideoFile()) { Core::App().showDocument( document, session->data().message(itemId)); } else if (document->isSong()) { Media::Player::instance()->play({ document, itemId }); } return true; } return false; } } // namespace const std::vector &LocalUrlHandlers() { static auto Result = std::vector{ { qsl("^join/?\\?invite=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"), JoinGroupByHash }, { qsl("^addstickers/?\\?set=([a-zA-Z0-9\\.\\_]+)(&|$)"), ShowStickerSet }, { qsl("^addtheme/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"), ShowTheme }, { qsl("^setlanguage/?(\\?lang=([a-zA-Z0-9\\.\\_\\-]+))?(&|$)"), SetLanguage }, { qsl("^msg_url/?\\?(.+)(#|$)"), ShareUrl }, { qsl("^confirmphone/?\\?(.+)(#|$)"), ConfirmPhone }, { qsl("^share_game_score/?\\?(.+)(#|$)"), ShareGameScore }, { qsl("^socks/?\\?(.+)(#|$)"), ApplySocksProxy }, { qsl("^proxy/?\\?(.+)(#|$)"), ApplyMtprotoProxy }, { qsl("^passport/?\\?(.+)(#|$)"), ShowPassport }, { qsl("^bg/?\\?(.+)(#|$)"), ShowWallPaper }, { qsl("^resolve/?\\?(.+)(#|$)"), ResolveUsername }, { qsl("^privatepost/?\\?(.+)(#|$)"), ResolvePrivatePost }, { qsl("^settings(/folders|/devices|/language)?$"), ResolveSettings }, { qsl("^([^\\?]+)(\\?|#|$)"), HandleUnknown }, }; return Result; } const std::vector &InternalUrlHandlers() { static auto Result = std::vector{ { qsl("^media_timestamp/?\\?base=([a-zA-Z0-9\\.\\_\\-]+)&t=(\\d+)(&|$)"), OpenMediaTimestamp }, }; return Result; } QString TryConvertUrlToLocal(QString url) { if (url.size() > 8192) { url = url.mid(0, 8192); } using namespace qthelp; auto matchOptions = RegExOption::CaseInsensitive; auto telegramMeMatch = regex_match(qsl("^(https?://)?(www\\.)?(telegram\\.(me|dog)|t\\.me)/(.+)$"), url, matchOptions); if (telegramMeMatch) { auto query = telegramMeMatch->capturedRef(5); if (auto joinChatMatch = regex_match(qsl("^joinchat/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"), query, matchOptions)) { return qsl("tg://join?invite=") + url_encode(joinChatMatch->captured(1)); } else if (auto stickerSetMatch = regex_match(qsl("^addstickers/([a-zA-Z0-9\\.\\_]+)(\\?|$)"), query, matchOptions)) { return qsl("tg://addstickers?set=") + url_encode(stickerSetMatch->captured(1)); } else if (auto themeMatch = regex_match(qsl("^addtheme/([a-zA-Z0-9\\.\\_]+)(\\?|$)"), query, matchOptions)) { return qsl("tg://addtheme?slug=") + url_encode(themeMatch->captured(1)); } else if (auto languageMatch = regex_match(qsl("^setlanguage/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"), query, matchOptions)) { return qsl("tg://setlanguage?lang=") + url_encode(languageMatch->captured(1)); } else if (auto shareUrlMatch = regex_match(qsl("^share/url/?\\?(.+)$"), query, matchOptions)) { return qsl("tg://msg_url?") + shareUrlMatch->captured(1); } else if (auto confirmPhoneMatch = regex_match(qsl("^confirmphone/?\\?(.+)"), query, matchOptions)) { return qsl("tg://confirmphone?") + confirmPhoneMatch->captured(1); } else if (auto ivMatch = regex_match(qsl("^iv/?\\?(.+)(#|$)"), query, matchOptions)) { // // We need to show our t.me page, not the url directly. // //auto params = url_parse_params(ivMatch->captured(1), UrlParamNameTransform::ToLower); //auto previewedUrl = params.value(qsl("url")); //if (previewedUrl.startsWith(qstr("http://"), Qt::CaseInsensitive) // || previewedUrl.startsWith(qstr("https://"), Qt::CaseInsensitive)) { // return previewedUrl; //} return url; } else if (auto socksMatch = regex_match(qsl("^socks/?\\?(.+)(#|$)"), query, matchOptions)) { return qsl("tg://socks?") + socksMatch->captured(1); } else if (auto proxyMatch = regex_match(qsl("^proxy/?\\?(.+)(#|$)"), query, matchOptions)) { return qsl("tg://proxy?") + proxyMatch->captured(1); } else if (auto bgMatch = regex_match(qsl("^bg/([a-zA-Z0-9\\.\\_\\-]+)(\\?(.+)?)?$"), query, matchOptions)) { const auto params = bgMatch->captured(3); return qsl("tg://bg?slug=") + bgMatch->captured(1) + (params.isEmpty() ? QString() : '&' + params); } else if (auto postMatch = regex_match(qsl("^c/(\\-?\\d+)/(\\d+)(/?\\?|/?$)"), query, matchOptions)) { auto params = query.mid(postMatch->captured(0).size()).toString(); return qsl("tg://privatepost?channel=%1&post=%2").arg(postMatch->captured(1)).arg(postMatch->captured(2)) + (params.isEmpty() ? QString() : '&' + params); } else if (auto usernameMatch = regex_match(qsl("^([a-zA-Z0-9\\.\\_]+)(/?\\?|/?$|/(\\d+)/?(?:\\?|$))"), query, matchOptions)) { auto params = query.mid(usernameMatch->captured(0).size()).toString(); auto postParam = QString(); if (auto postMatch = regex_match(qsl("^/\\d+/?(?:\\?|$)"), usernameMatch->captured(2))) { postParam = qsl("&post=") + usernameMatch->captured(3); } return qsl("tg://resolve/?domain=") + url_encode(usernameMatch->captured(1)) + postParam + (params.isEmpty() ? QString() : '&' + params); } } return url; } bool InternalPassportLink(const QString &url) { const auto urlTrimmed = url.trimmed(); if (!urlTrimmed.startsWith(qstr("tg://"), Qt::CaseInsensitive)) { return false; } const auto command = urlTrimmed.midRef(qstr("tg://").size()); using namespace qthelp; const auto matchOptions = RegExOption::CaseInsensitive; const auto authMatch = regex_match( qsl("^passport/?\\?(.+)(#|$)"), command, matchOptions); const auto usernameMatch = regex_match( qsl("^resolve/?\\?(.+)(#|$)"), command, matchOptions); const auto usernameValue = usernameMatch->hasMatch() ? url_parse_params( usernameMatch->captured(1), UrlParamNameTransform::ToLower).value(qsl("domain")) : QString(); const auto authLegacy = (usernameValue == qstr("telegrampassport")); return authMatch->hasMatch() || authLegacy; } bool StartUrlRequiresActivate(const QString &url) { return Core::App().passcodeLocked() ? true : !InternalPassportLink(url); } } // namespace Core