diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 9963d241ac..fbdec9dc9f 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -444,6 +444,8 @@ PRIVATE data/data_audio_msg_id.h data/data_auto_download.cpp data/data_auto_download.h + data/data_bot_app.cpp + data/data_bot_app.h data/data_chat.cpp data/data_chat.h data/data_chat_filters.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1dcc3d5fd7..b76a04c586 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -396,6 +396,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_username_available" = "This username is available."; "lng_username_not_found" = "User @{user} not found."; "lng_username_by_phone_not_found" = "User {phone} not found."; +"lng_username_app_not_found" = "Bot application not found."; "lng_username_link" = "This link opens a chat with you:"; "lng_username_copied" = "Link copied to clipboard."; @@ -1463,6 +1464,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_took_screenshot" = "{from} took a screenshot!"; "lng_action_you_took_screenshot" = "You took a screenshot!"; "lng_action_bot_allowed_from_domain" = "You allowed this bot to message you when you logged in on {domain}."; +"lng_action_bot_allowed_from_app" = "You allowed this bot to message you when you opened {app}."; "lng_action_secure_values_sent" = "{user} received the following documents: {documents}"; "lng_action_secure_personal_details" = "personal details"; "lng_action_secure_proof_of_identity" = "proof of identity"; diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 6f080e06d5..39ba3369ec 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -108,7 +108,11 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) { }; if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive) || url.startsWith(u"internal:"_q, Qt::CaseInsensitive)) { - open(); + UrlClickHandler::Open(url, QVariant::fromValue([&] { + auto result = context.value(); + result.mayShowConfirmation = !base::IsCtrlPressed(); + return result; + }())); } else { const auto parsedUrl = QUrl::fromUserInput(url); if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) { diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 9157b17836..de57083220 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -42,6 +42,7 @@ struct ClickHandlerContext { Fn elementDelegate; base::weak_ptr sessionWindow; std::shared_ptr show; + bool mayShowConfirmation = false; bool skipBotAutoLogin = false; bool botStartAutoSubmit = false; // Is filled from peer info. diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 2e2034e025..30d4211607 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -373,6 +373,8 @@ bool ResolveUsernameOrPhone( if (const auto postId = postParam.toInt()) { post = postId; } + const auto appname = params.value(u"appname"_q); + const auto appstart = params.value(u"startapp"_q); const auto commentParam = params.value(u"comment"_q); const auto commentId = commentParam.toInt(); const auto topicParam = params.value(u"topic"_q); @@ -384,6 +386,12 @@ bool ResolveUsernameOrPhone( startToken = gameParam; resolveType = ResolveType::ShareGame; } + if (startToken.isEmpty() && params.contains(u"startapp"_q)) { + startToken = params.value(u"startapp"_q); + } + if (!appname.isEmpty()) { + resolveType = ResolveType::BotApp; + } const auto myContext = context.value(); using Navigation = Window::SessionNavigation; controller->showPeerByLink(Navigation::PeerByLinkInfo{ @@ -403,6 +411,8 @@ bool ResolveUsernameOrPhone( .startToken = startToken, .startAdminRights = adminRights, .startAutoSubmit = myContext.botStartAutoSubmit, + .botAppName = appname.isEmpty() ? postParam : appname, + .botAppForceConfirmation = myContext.mayShowConfirmation, .attachBotUsername = params.value(u"attach"_q), .attachBotToggleCommand = (params.contains(u"startattach"_q) ? params.value(u"startattach"_q) @@ -1004,6 +1014,7 @@ QString TryConvertUrlToLocal(QString url) { "(" "/?\\?|" "/?$|" + "/[a-zA-Z0-9\\.\\_]+|" "/\\d+/?(\\?|$)|" "/\\d+/\\d+/?(\\?|$)" ")"_q, query, matchOptions)) { @@ -1014,6 +1025,8 @@ QString TryConvertUrlToLocal(QString url) { added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2)); } else if (const auto postMatch = regex_match(u"^/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { added = u"&post="_q + postMatch->captured(1); + } else if (const auto appNameMatch = regex_match(u"^/([a-zA-Z0-9\\.\\_]+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { + added = u"&appname="_q + appNameMatch->captured(1); } return base + added + (params.isEmpty() ? QString() : '&' + params); } diff --git a/Telegram/SourceFiles/data/data_bot_app.cpp b/Telegram/SourceFiles/data/data_bot_app.cpp new file mode 100644 index 0000000000..ff9705cb64 --- /dev/null +++ b/Telegram/SourceFiles/data/data_bot_app.cpp @@ -0,0 +1,13 @@ +/* +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_bot_app.h" + +BotAppData::BotAppData(not_null owner, const BotAppId &id) +: owner(owner) +, id(id) { +} diff --git a/Telegram/SourceFiles/data/data_bot_app.h b/Telegram/SourceFiles/data/data_bot_app.h new file mode 100644 index 0000000000..7de4aefe86 --- /dev/null +++ b/Telegram/SourceFiles/data/data_bot_app.h @@ -0,0 +1,27 @@ +/* +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 "data/data_photo.h" +#include "data/data_document.h" + +struct BotAppData { + BotAppData(not_null owner, const BotAppId &id); + + const not_null owner; + BotAppId id = 0; + PeerId botId = 0; + QString shortName; + QString title; + QString description; + PhotoData *photo = nullptr; + DocumentData *document = nullptr; + + uint64 accessHash = 0; + uint64 hash = 0; +}; diff --git a/Telegram/SourceFiles/data/data_game.h b/Telegram/SourceFiles/data/data_game.h index ab41833252..b71fdd0f2c 100644 --- a/Telegram/SourceFiles/data/data_game.h +++ b/Telegram/SourceFiles/data/data_game.h @@ -21,5 +21,4 @@ struct GameData { QString description; PhotoData *photo = nullptr; DocumentData *document = nullptr; - }; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index d8afb561f1..616d311941 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name #include "data/stickers/data_stickers.h" #include "data/notify/data_notify_settings.h" +#include "data/data_bot_app.h" #include "data/data_changes.h" #include "data/data_group_call.h" #include "data/data_media_types.h" @@ -3450,6 +3451,45 @@ void Session::gameApplyFields( notifyGameUpdateDelayed(game); } +not_null Session::botApp(BotAppId id) { + const auto i = _botApps.find(id); + return (i != end(_botApps)) + ? i->second.get() + : _botApps.emplace( + id, + std::make_unique(this, id)).first->second.get(); +} + +BotAppData *Session::findBotApp(PeerId botId, const QString &appName) const { + for (const auto &[id, app] : _botApps) { + if (app->botId == botId && app->shortName == appName) { + return app.get(); + } + } + return nullptr; +} + +BotAppData *Session::processBotApp( + PeerId botId, + const MTPBotApp &data) { + return data.match([&](const MTPDbotApp &data) { + const auto result = botApp(data.vid().v); + result->botId = botId; + result->shortName = qs(data.vshort_name()); + result->title = qs(data.vtitle()); + result->description = qs(data.vdescription()); + result->photo = processPhoto(data.vphoto()); + result->document = data.vdocument() + ? processDocument(*data.vdocument()).get() + : nullptr; + result->accessHash = data.vaccess_hash().v; + result->hash = data.vhash().v; + return result.get(); + }, [](const MTPDbotAppNotModified &) { + return (BotAppData*)nullptr; + }); +} + not_null Session::poll(PollId id) { auto i = _polls.find(id); if (i == _polls.cend()) { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 776c9cadcd..16c56c23ee 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -570,6 +570,12 @@ public: not_null original, const MTPGame &data); + [[nodiscard]] not_null botApp(BotAppId id); + BotAppData *findBotApp(PeerId botId, const QString &appName) const; + BotAppData *processBotApp( + PeerId botId, + const MTPBotApp &data); + [[nodiscard]] not_null poll(PollId id); not_null processPoll(const MTPPoll &data); not_null processPoll(const MTPDmessageMediaPoll &data); @@ -922,6 +928,9 @@ private: std::unordered_map< GameId, std::unique_ptr> _games; + std::unordered_map< + BotAppId, + std::unique_ptr> _botApps; std::unordered_map< not_null, base::flat_set>> _gameViews; diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index faa1fd27df..375e9da07f 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -118,6 +118,7 @@ class DocumentData; class PhotoData; struct WebPageData; struct GameData; +struct BotAppData; struct PollData; using PhotoId = uint64; @@ -129,6 +130,7 @@ using GameId = uint64; using PollId = uint64; using WallPaperId = uint64; using CallId = uint64; +using BotAppId = uint64; constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL); struct PreparedPhotoThumb { diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 17d1c84b6e..e8d9b86bfc 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1059,6 +1059,12 @@ ServiceAction ParseServiceAction( result.content = content; }, [&](const MTPDmessageActionBotAllowed &data) { auto content = ActionBotAllowed(); + if (const auto app = data.vapp()) { + app->match([&](const MTPDbotApp &data) { + content.appId = data.vid().v; + content.app = ParseString(data.vtitle()); + }, [](const MTPDbotAppNotModified &) {}); + } if (const auto domain = data.vdomain()) { content.domain = ParseString(*domain); } diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index ee665c79ed..71bae17f82 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -431,6 +431,8 @@ struct ActionCustomAction { }; struct ActionBotAllowed { + uint64 appId = 0; + Utf8String app; Utf8String domain; bool attachMenu = false; }; diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 6044ef33c3..59c176bae9 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1033,6 +1033,9 @@ auto HtmlWriter::Wrap::pushMessage( return data.attachMenu ? "You allowed this bot to message you " "when you added it in the attachment menu."_q + : data.app.isEmpty() + ? ("You allowed this bot to message you when you opened " + + SerializeString(data.app)) : ("You allowed this bot to message you when you logged in on " + SerializeString(data.domain)); }, [&](const ActionSecureValuesSent &data) { diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index eccb6bb0b1..e24a364ba4 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -477,6 +477,10 @@ QByteArray SerializeMessage( }, [&](const ActionBotAllowed &data) { if (data.attachMenu) { pushAction("attach_menu_bot_allowed"); + } else if (data.appId) { + pushAction("allow_sending_messages"); + push("reason_app_id", data.appId); + push("reason_app_name", data.app); } else { pushAction("allow_sending_messages"); push("reason_domain", data.domain); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index ce595def5a..2a312bb4a3 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -49,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_updates.h" #include "dialogs/ui/dialogs_message_view.h" #include "data/notify/data_notify_settings.h" +#include "data/data_bot_app.h" #include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp #include "data/data_changes.h" #include "data/data_session.h" @@ -3689,6 +3690,21 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { result.text = { tr::lng_action_attach_menu_bot_allowed(tr::now) }; + } else if (const auto app = action.vapp()) { + const auto bot = history()->peer->asUser(); + const auto botId = bot ? bot->id : PeerId(); + const auto info = history()->owner().processBotApp(botId, *app); + const auto url = (bot && info) + ? history()->session().createInternalLinkFull( + bot->username() + '/' + info->shortName) + : QString(); + result.text = tr::lng_action_bot_allowed_from_app( + tr::now, + lt_app, + (url.isEmpty() + ? TextWithEntities{ u"App"_q } + : Ui::Text::Link(info->title, url)), + Ui::Text::WithEntities); } else { const auto domain = qs(action.vdomain().value_or_empty()); result.text = tr::lng_action_bot_allowed_from_domain( diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 9edb594c9b..a78d465671 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "inline_bots/bot_attach_web_view.h" #include "api/api_common.h" +#include "data/data_bot_app.h" #include "data/data_user.h" #include "data/data_file_origin.h" #include "data/data_document.h" @@ -424,6 +425,7 @@ struct AttachWebView::Context { Dialogs::EntryState dialogsEntryState; Api::SendAction action; bool fromSwitch = false; + bool fromBotApp = false; }; AttachWebView::AttachWebView(not_null session) @@ -551,13 +553,12 @@ void AttachWebView::request(const WebViewButton &button) { : MTP_inputPeerEmpty()) )).done([=](const MTPWebViewResult &result) { _requestId = 0; - result.match([&](const MTPDwebViewResultUrl &data) { - show( - data.vquery_id().v, - qs(data.vurl()), - button.text, - button.fromMenu || button.url.isEmpty()); - }); + const auto &data = result.data(); + show( + data.vquery_id().v, + qs(data.vurl()), + button.text, + button.fromMenu || button.url.isEmpty()); }).fail([=](const MTP::Error &error) { _requestId = 0; if (error.type() == u"BOT_INVALID"_q) { @@ -573,7 +574,9 @@ void AttachWebView::cancel() { _panel = nullptr; _context = nullptr; _bot = nullptr; + _app = nullptr; _botUsername = QString(); + _botAppName = QString(); _startCommand = QString(); } @@ -624,9 +627,7 @@ void AttachWebView::requestAddToMenu( Expects(controller != nullptr || _context != nullptr); if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) { - Ui::ShowMultilineToast({ - .text = { tr::lng_bot_menu_not_supported(tr::now) }, - }); + showToast(tr::lng_bot_menu_not_supported(tr::now), controller); return; } const auto wasController = (controller != nullptr); @@ -694,10 +695,8 @@ void AttachWebView::requestAddToMenu( } else { requestBots(); if (!open(types)) { - Ui::ShowMultilineToast({ - .text = { - tr::lng_bot_menu_already_added(tr::now) }, - }); + showToast( + tr::lng_bot_menu_already_added(tr::now)); } } } @@ -708,17 +707,13 @@ void AttachWebView::requestAddToMenu( _addToMenuBot = nullptr; _addToMenuContext = nullptr; _addToMenuStartCommand = QString(); - Ui::ShowMultilineToast({ - .text = { tr::lng_bot_menu_not_supported(tr::now) }, - }); + showToast(tr::lng_bot_menu_not_supported(tr::now)); }).send(); } void AttachWebView::removeFromMenu(not_null bot) { toggleInMenu(bot, ToggledState::Removed, [=] { - Ui::ShowMultilineToast({ - .text = { tr::lng_bot_remove_from_menu_done(tr::now) }, - }); + showToast(tr::lng_bot_remove_from_menu_done(tr::now)); }); } @@ -729,9 +724,7 @@ void AttachWebView::resolve() { } _bot = bot->asUser(); if (!_bot) { - Ui::ShowMultilineToast({ - .text = { tr::lng_bot_menu_not_supported(tr::now) } - }); + showToast(tr::lng_bot_menu_not_supported(tr::now)); return; } requestAddToMenu(_bot, _startCommand); @@ -760,11 +753,8 @@ void AttachWebView::resolveUsername( }).fail([=](const MTP::Error &error) { _requestId = 0; if (error.code() == 400) { - Ui::ShowMultilineToast({ - .text = { - tr::lng_username_not_found(tr::now, lt_user, username), - }, - }); + showToast( + tr::lng_username_not_found(tr::now, lt_user, username)); } }).send(); } @@ -838,9 +828,8 @@ void AttachWebView::requestMenu( : MTP_inputPeerEmpty()) )).done([=](const MTPWebViewResult &result) { _requestId = 0; - result.match([&](const MTPDwebViewResultUrl &data) { - show(data.vquery_id().v, qs(data.vurl()), text); - }); + const auto &data = result.data(); + show(data.vquery_id().v, qs(data.vurl()), text); }).fail([=](const MTP::Error &error) { _requestId = 0; if (error.type() == u"BOT_INVALID"_q) { @@ -850,6 +839,129 @@ void AttachWebView::requestMenu( }); } +void AttachWebView::requestApp( + not_null controller, + const Api::SendAction &action, + not_null bot, + const QString &appName, + const QString &startParam, + bool forceConfirmation) { + const auto context = LookupContext(controller, action); + if (_requestId + && _bot == bot + && _startCommand == startParam + && _botAppName == appName + && IsSame(_context, context)) { + return; + } + cancel(); + _bot = bot; + _startCommand = startParam; + _botAppName = appName; + _context = std::make_unique(context); + _context->fromBotApp = true; + const auto already = _session->data().findBotApp(_bot->id, appName); + _requestId = _session->api().request(MTPmessages_GetBotApp( + MTP_inputBotAppShortName( + bot->inputUser, + MTP_string(appName)), + MTP_long(already ? already->hash : 0) + )).done([=](const MTPmessages_BotApp &result) { + _requestId = 0; + if (!_bot || !_context) { + return; + } + const auto &data = result.data(); + const auto firstTime = data.is_inactive(); + const auto received = _session->data().processBotApp( + _bot->id, + data.vapp()); + _app = received ? received : already; + if (!_app) { + cancel(); + showToast(tr::lng_username_app_not_found(tr::now)); + return; + } + const auto confirm = firstTime || forceConfirmation; + if (confirm) { + confirmAppOpen(result.data().is_request_write_access()); + } else { + requestAppView(false); + } + }).fail([=] { + cancel(); + showToast(tr::lng_username_app_not_found(tr::now)); + }).send(); +} + +void AttachWebView::confirmAppOpen(bool requestWriteAccess) { + const auto controller = _context ? _context->controller.get() : nullptr; + if (!controller || !_bot) { + return; + } + controller->show(Box([=](not_null box) { + const auto allowed = std::make_shared(); + const auto done = [=](Fn close) { + requestAppView((*allowed) && (*allowed)->checked()); + close(); + }; + Ui::ConfirmBox(box, { + tr::lng_allow_bot_webview( + tr::now, + lt_bot_name, + Ui::Text::Bold(_bot->name()), + Ui::Text::RichLangValue), + done, + }); + if (requestWriteAccess) { + (*allowed) = box->addRow( + object_ptr( + box, + tr::lng_url_auth_allow_messages( + tr::now, + lt_bot, + Ui::Text::Bold(_bot->name()), + Ui::Text::WithEntities), + true, + st::urlAuthCheckbox), + style::margins( + st::boxRowPadding.left(), + st::boxPhotoCaptionSkip, + st::boxRowPadding.right(), + st::boxPhotoCaptionSkip)); + (*allowed)->setAllowTextLines(); + } + })); +} + +void AttachWebView::requestAppView(bool allowWrite) { + if (!_context || !_app) { + return; + } + using Flag = MTPmessages_RequestAppWebView::Flag; + const auto flags = Flag::f_theme_params + | (_startCommand.isEmpty() ? Flag(0) : Flag::f_start_param) + | (allowWrite ? Flag::f_write_allowed : Flag(0)); + _requestId = _session->api().request(MTPmessages_RequestAppWebView( + MTP_flags(flags), + _context->action.history->peer->input, + MTP_inputBotAppID(MTP_long(_app->id), MTP_long(_app->accessHash)), + MTP_string(_startCommand), + MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), + MTP_string("tdesktop") + )).done([=](const MTPAppWebViewResult &result) { + _requestId = 0; + const auto &data = result.data(); + const auto queryId = uint64(); + show(queryId, qs(data.vurl())); + }).fail([=](const MTP::Error &error) { + _requestId = 0; + if (error.type() == u"BOT_INVALID"_q) { + requestBots(); + } + }).send(); +} + void AttachWebView::confirmOpen( not_null controller, Fn done) { @@ -895,6 +1007,7 @@ void AttachWebView::show( const auto sendData = crl::guard(this, [=](QByteArray data) { if (!_context || _context->fromSwitch + || _context->fromBotApp || _context->action.history->peer != _bot || queryId) { return; @@ -1061,7 +1174,7 @@ void AttachWebView::show( void AttachWebView::started(uint64 queryId) { Expects(_bot != nullptr && _context != nullptr); - if (_context->fromSwitch) { + if (_context->fromSwitch || !queryId) { return; } @@ -1098,6 +1211,24 @@ void AttachWebView::started(uint64 queryId) { }, _panel->lifetime()); } +void AttachWebView::showToast( + const QString &text, + Window::SessionController *controller) { + const auto strong = controller + ? controller + : _context + ? _context->controller.get() + : _addToMenuContext + ? _addToMenuContext->controller.get() + : nullptr; + Ui::ShowMultilineToast({ + .parentOverride = (strong + ? Window::Show(strong).toastParent().get() + : nullptr), + .text = { text }, + }); +} + void AttachWebView::confirmAddToMenu( AttachWebViewBot bot, Fn callback) { @@ -1115,9 +1246,7 @@ void AttachWebView::confirmAddToMenu( if (callback) { callback(); } - Ui::ShowMultilineToast({ - .text = { tr::lng_bot_add_to_menu_done(tr::now) }, - }); + showToast(tr::lng_bot_add_to_menu_done(tr::now)); }); close(); }; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 8f960b492a..658f59940b 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -93,6 +93,13 @@ public: void requestMenu( not_null controller, not_null bot); + void requestApp( + not_null controller, + const Api::SendAction &action, + not_null bot, + const QString &appName, + const QString &startParam, + bool forceConfirmation); void cancel(); @@ -162,14 +169,22 @@ private: void confirmAddToMenu( AttachWebViewBot bot, Fn callback = nullptr); + void confirmAppOpen(bool requestWriteAccess); + void requestAppView(bool allowWrite); void started(uint64 queryId); + void showToast( + const QString &text, + Window::SessionController *controller = nullptr); + const not_null _session; std::unique_ptr _context; UserData *_bot = nullptr; QString _botUsername; + QString _botAppName; QString _startCommand; + BotAppData *_app = nullptr; QPointer _confirmAddBox; mtpRequestId _requestId = 0; diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp index 90af03e55d..9085fe619e 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp @@ -447,7 +447,7 @@ void Inner::refreshMosaicOffset() { const auto top = _switchPmButton ? (_switchPmButton->height() + st::inlineResultsSkip) : 0; - _mosaic.setPadding(st::gifsPadding + QMargins(0, top, 0, 0)); + _mosaic.setPadding(st::emojiPanMargins + QMargins(0, top, 0, 0)); } void Inner::refreshSwitchPmButton(const CacheEntry *entry) { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 4584cb8886..c95ab613da 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -357,6 +357,15 @@ void SessionNavigation::showPeerByLinkResolved( using Scope = AddBotToGroupBoxController::Scope; const auto user = peer->asUser(); const auto bot = (user && user->isBot()) ? user : nullptr; + + // t.me/username/012345 - we thought it was a channel post link, but + // after resolving the username we found out it is a bot. + const auto resolveType = (bot + && !info.botAppName.isEmpty() + && info.resolveType == ResolveType::Default) + ? ResolveType::BotApp + : info.resolveType; + const auto &replies = info.repliesInfo; if (const auto threadId = std::get_if(&replies)) { showRepliesForMessage( @@ -389,14 +398,29 @@ void SessionNavigation::showPeerByLinkResolved( info.messageId, callback); } - } else if (bot && info.resolveType == ResolveType::ShareGame) { + } else if (bot && resolveType == ResolveType::BotApp) { + const auto itemId = info.clickFromMessageId; + const auto item = _session->data().message(itemId); + const auto contextPeer = item + ? item->history()->peer + : bot; + crl::on_main(this, [=] { + bot->session().attachWebView().requestApp( + parentController(), + Api::SendAction(bot->owner().history(contextPeer)), + bot, + info.botAppName, + info.startToken, + info.botAppForceConfirmation); + }); + } else if (bot && resolveType == ResolveType::ShareGame) { Window::ShowShareGameBox(parentController(), bot, info.startToken); } else if (bot - && (info.resolveType == ResolveType::AddToGroup - || info.resolveType == ResolveType::AddToChannel)) { - const auto scope = (info.resolveType == ResolveType::AddToGroup) + && (resolveType == ResolveType::AddToGroup + || resolveType == ResolveType::AddToChannel)) { + const auto scope = (resolveType == ResolveType::AddToGroup) ? (info.startAdminRights ? Scope::GroupAdmin : Scope::All) - : (info.resolveType == ResolveType::AddToChannel) + : (resolveType == ResolveType::AddToChannel) ? Scope::ChannelAdmin : Scope::None; Assert(scope != Scope::None); @@ -407,7 +431,7 @@ void SessionNavigation::showPeerByLinkResolved( scope, info.startToken, info.startAdminRights); - } else if (info.resolveType == ResolveType::Mention) { + } else if (resolveType == ResolveType::Mention) { if (bot || peer->isChannel()) { crl::on_main(this, [=] { showPeerHistory(peer, params); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index d83adab467..8014765f65 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -97,6 +97,7 @@ inline constexpr bool is_flag_type(GifPauseReason) { return true; }; enum class ResolveType { Default, + BotApp, BotStart, AddToGroup, AddToChannel, @@ -208,6 +209,8 @@ public: QString startToken; ChatAdminRights startAdminRights; bool startAutoSubmit = false; + QString botAppName; + bool botAppForceConfirmation = false; QString attachBotUsername; std::optional attachBotToggleCommand; InlineBots::PeerTypes attachBotChooseTypes;