diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index a045ba1f0..4b314e4e9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1611,6 +1611,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_suggested_video" = "{user} suggests you to use this profile video."; "lng_action_suggested_video_button" = "View Video"; "lng_action_attach_menu_bot_allowed" = "You allowed this bot to message you when you added it in the attachment menu."; +"lng_action_webapp_bot_allowed" = "You allowed this bot to message you in his web-app."; "lng_action_set_wallpaper_me" = "You set a new wallpaper for this chat"; "lng_action_set_wallpaper" = "{user} set a new wallpaper for this chat"; "lng_action_set_wallpaper_button" = "View Wallpaper"; @@ -1799,6 +1800,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_share_location_unavailable" = "Sorry, location sharing is currently unavailable in Telegram Desktop."; "lng_bot_share_phone" = "Do you want to share your phone number with this bot?"; "lng_bot_share_phone_confirm" = "Share"; +"lng_bot_allow_write_title" = "Allow messaging"; +"lng_bot_allow_write" = "Do you want to allow this bot writing you?"; +"lng_bot_allow_write_confirm" = "Allow"; "lng_attach_failed" = "Failed"; "lng_attach_file" = "File"; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index aebc9b809..2bea4bba8 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3316,24 +3316,24 @@ void ApiWrap::forwardMessages( _session->data().sendHistoryChangeNotifications(); } -void ApiWrap::shareContact( +FullMsgId ApiWrap::shareContact( const QString &phone, const QString &firstName, const QString &lastName, const SendAction &action) { const auto userId = UserId(0); - sendSharedContact(phone, firstName, lastName, userId, action); + return sendSharedContact(phone, firstName, lastName, userId, action); } -void ApiWrap::shareContact( +FullMsgId ApiWrap::shareContact( not_null user, const SendAction &action) { const auto userId = peerToUser(user->id); const auto phone = _session->data().findContactPhone(user); if (phone.isEmpty()) { - return; + return {}; } - sendSharedContact( + return sendSharedContact( phone, user->firstName, user->lastName, @@ -3341,7 +3341,7 @@ void ApiWrap::shareContact( action); } -void ApiWrap::sendSharedContact( +FullMsgId ApiWrap::sendSharedContact( const QString &phone, const QString &firstName, const QString &lastName, @@ -3391,6 +3391,7 @@ void ApiWrap::sendSharedContact( MTP_string(), // vcard MTP_long(userId.bare)), HistoryMessageMarkupData()); + const auto result = item->fullId(); const auto media = MTP_inputMediaContact( MTP_string(phone), @@ -3405,6 +3406,8 @@ void ApiWrap::sendSharedContact( (action.options.scheduled ? Data::HistoryUpdate::Flag::ScheduledSent : Data::HistoryUpdate::Flag::MessageSent)); + + return result; } void ApiWrap::sendVoiceMessage( diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 954500f76..b064d8df7 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -290,12 +290,14 @@ public: Data::ResolvedForwardDraft &&draft, const SendAction &action, FnMut &&successCallback = nullptr); - void shareContact( + FullMsgId shareContact( const QString &phone, const QString &firstName, const QString &lastName, const SendAction &action); - void shareContact(not_null user, const SendAction &action); + FullMsgId shareContact( + not_null user, + const SendAction &action); void applyAffectedMessages( not_null peer, const MTPmessages_AffectedMessages &result); @@ -484,7 +486,7 @@ private: SharedMediaType type, Api::SearchResult &&parsed); - void sendSharedContact( + FullMsgId sendSharedContact( const QString &phone, const QString &firstName, const QString &lastName, diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index ead942262..8f1be8324 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -370,7 +370,6 @@ QString UiIntegration::phrasePanelCloseAnyway() { return tr::lng_bot_close_warning_sure(tr::now); } -#if 0 // disabled for now QString UiIntegration::phraseBotSharePhone() { return tr::lng_bot_share_phone(tr::now); } @@ -382,7 +381,18 @@ QString UiIntegration::phraseBotSharePhoneTitle() { QString UiIntegration::phraseBotSharePhoneConfirm() { return tr::lng_bot_share_phone_confirm(tr::now); } -#endif + +QString UiIntegration::phraseBotAllowWrite() { + return tr::lng_bot_allow_write(tr::now); +} + +QString UiIntegration::phraseBotAllowWriteTitle() { + return tr::lng_bot_allow_write_title(tr::now); +} + +QString UiIntegration::phraseBotAllowWriteConfirm() { + return tr::lng_bot_allow_write_confirm(tr::now); +} bool OpenGLLastCheckFailed() { return QFile::exists(OpenGLCheckFilePath()); diff --git a/Telegram/SourceFiles/core/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h index 65661ecfd..cd3e4ff58 100644 --- a/Telegram/SourceFiles/core/ui_integration.h +++ b/Telegram/SourceFiles/core/ui_integration.h @@ -84,11 +84,12 @@ public: QString phrasePanelCloseWarning() override; QString phrasePanelCloseUnsaved() override; QString phrasePanelCloseAnyway() override; -#if 0 // disabled for now QString phraseBotSharePhone() override; QString phraseBotSharePhoneTitle() override; QString phraseBotSharePhoneConfirm() override; -#endif + QString phraseBotAllowWrite() override; + QString phraseBotAllowWriteTitle() override; + QString phraseBotAllowWriteConfirm() override; }; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 6b9fc2997..723eeae9b 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1173,6 +1173,7 @@ ServiceAction ParseServiceAction( content.domain = ParseString(*domain); } content.attachMenu = data.is_attach_menu(); + content.fromRequest = data.is_from_request(); result.content = content; }, [&](const MTPDmessageActionSecureValuesSentMe &data) { // Should not be in user inbox. diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 27fb14ea1..4635ea269 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -439,6 +439,7 @@ struct ActionBotAllowed { Utf8String app; Utf8String domain; bool attachMenu = false; + bool fromRequest = false; }; struct ActionSecureValuesSent { diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index efa4a7599..d4581c737 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1127,6 +1127,8 @@ auto HtmlWriter::Wrap::pushMessage( return data.attachMenu ? "You allowed this bot to message you " "when you added it in the attachment menu."_q + : data.fromRequest + ? "You allowed this bot to message you in his web-app."_q : data.app.isEmpty() ? ("You allowed this bot to message you when you opened " + SerializeString(data.app)) diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 9dfd21620..a232b2788 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -477,6 +477,8 @@ QByteArray SerializeMessage( }, [&](const ActionBotAllowed &data) { if (data.attachMenu) { pushAction("attach_menu_bot_allowed"); + } else if (data.fromRequest) { + pushAction("web_app_bot_allowed"); } else if (data.appId) { pushAction("allow_sending_messages"); push("reason_app_id", data.appId); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index ff5aa329b..d81196689 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3887,6 +3887,10 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { result.text = { tr::lng_action_attach_menu_bot_allowed(tr::now) }; + } else if (action.is_from_request()) { + result.text = { + tr::lng_action_webapp_bot_allowed(tr::now) + }; } else if (const auto app = action.vapp()) { const auto bot = history()->peer->asUser(); const auto botId = bot ? bot->id : PeerId(); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index be7a81472..410fa6b79 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_common.h" #include "core/click_handler_types.h" #include "data/data_bot_app.h" +#include "data/data_changes.h" #include "data/data_user.h" #include "data/data_file_origin.h" #include "data/data_document.h" @@ -462,6 +463,226 @@ void AttachWebView::request( resolve(); } +Webview::ThemeParams AttachWebView::botThemeParams() { + return Window::Theme::WebViewParams(); +} + +bool AttachWebView::botHandleLocalUri(QString uri) { + const auto local = Core::TryConvertUrlToLocal(uri); + if (uri == local || Core::InternalPassportLink(local)) { + return local.startsWith(u"tg://"_q); + } else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive)) { + return false; + } + botClose(); + crl::on_main([=, shownUrl = _lastShownUrl] { + const auto variant = QVariant::fromValue(ClickHandlerContext{ + .attachBotWebviewUrl = shownUrl, + }); + UrlClickHandler::Open(local, variant); + }); + return true; +} + +void AttachWebView::botHandleInvoice(QString slug) { + Expects(_panel != nullptr); + + using Result = Payments::CheckoutResult; + const auto weak = base::make_weak(_panel.get()); + const auto reactivate = [=](Result result) { + if (const auto strong = weak.get()) { + strong->invoiceClosed(slug, [&] { + switch (result) { + case Result::Paid: return "paid"; + case Result::Failed: return "failed"; + case Result::Pending: return "pending"; + case Result::Cancelled: return "cancelled"; + } + Unexpected("Payments::CheckoutResult value."); + }()); + } + }; + _panel->hideForPayment(); + Payments::CheckoutProcess::Start(&_bot->session(), slug, reactivate); +} + +void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) { + Expects(_bot != nullptr); + Expects(_panel != nullptr); + + using Button = Ui::BotWebView::MenuButton; + const auto bot = _bot; + switch (button) { + case Button::OpenBot: + botClose(); + if (bot->session().windows().empty()) { + Core::App().domain().activate(&bot->session().account()); + } + if (!bot->session().windows().empty()) { + const auto window = bot->session().windows().front(); + window->showPeerHistory(bot); + window->window().activate(); + } + break; + case Button::RemoveFromMenu: + const auto attached = ranges::find( + _attachBots, + not_null{ _bot }, + &AttachWebViewBot::user); + const auto name = (attached != end(_attachBots)) + ? attached->name + : _bot->name(); + const auto done = crl::guard(this, [=] { + removeFromMenu(bot); + botClose(); + if (const auto active = Core::App().activeWindow()) { + active->activate(); + } + }); + _panel->showBox(Ui::MakeConfirmBox({ + tr::lng_bot_remove_from_menu_sure( + tr::now, + lt_bot, + Ui::Text::Bold(name), + Ui::Text::WithEntities), + done, + })); + break; + } +} + +void AttachWebView::botSendData(QByteArray data) { + if (!_context + || _context->fromSwitch + || _context->fromBotApp + || _context->action.history->peer != _bot + || _lastShownQueryId) { + return; + } + const auto randomId = base::RandomValue(); + _session->api().request(MTPmessages_SendWebViewData( + _bot->inputUser, + MTP_long(randomId), + MTP_string(_lastShownButtonText), + MTP_bytes(data) + )).done([=](const MTPUpdates &result) { + _session->api().applyUpdates(result); + }).send(); + crl::on_main(this, [=] { cancel(); }); +} + +void AttachWebView::botSwitchInlineQuery( + std::vector chatTypes, + QString query) { + const auto controller = _context + ? _context->controller.get() + : nullptr; + const auto types = PeerTypesFromNames(chatTypes); + if (!_bot + || !_bot->isBot() + || _bot->botInfo->inlinePlaceholder.isEmpty() + || !controller) { + return; + } else if (!types) { + if (_context->dialogsEntryState.key.owningHistory()) { + controller->switchInlineQuery( + _context->dialogsEntryState, + _bot, + query); + } + } else { + const auto bot = _bot; + const auto done = [=](not_null thread) { + controller->switchInlineQuery(thread, bot, query); + }; + ShowChooseBox( + controller, + types, + done, + tr::lng_inline_switch_choose()); + } + crl::on_main(this, [=] { cancel(); }); +} + +void AttachWebView::botCheckWriteAccess(Fn callback) { + _session->api().request(MTPbots_CanSendMessage( + _bot->inputUser + )).done([=](const MTPBool &result) { + callback(mtpIsTrue(result)); + }).fail([=] { + callback(false); + }).send(); +} + +void AttachWebView::botAllowWriteAccess(Fn callback) { + _session->api().request(MTPbots_AllowSendMessage( + _bot->inputUser + )).done([=](const MTPUpdates &result) { + _session->api().applyUpdates(result); + callback(true); + }).fail([=] { + callback(false); + }).send(); +} + +void AttachWebView::botSharePhone(Fn callback) { + const auto history = _bot->owner().history(_bot); + auto action = Api::SendAction(history); + action.clearDraft = false; + const auto id = history->session().api().shareContact( + _bot->session().user(), + action); + const auto owner = &_bot->owner(); + const auto lifetime = std::make_shared(); + const auto check = [=] { + const auto item = id ? owner->message(id) : nullptr; + if (!item || item->hasFailed()) { + lifetime->destroy(); + callback(false); + } + }; + + _bot->session().changes().historyUpdates( + history, + Data::HistoryUpdate::Flag::ClientSideMessages + ) | rpl::start_with_next(check, *lifetime); + + owner->itemRemoved( + ) | rpl::start_with_next([=](not_null item) { + if (item->fullId() == id) { + check(); + } + }, *lifetime); + + owner->itemIdChanged( + ) | rpl::start_with_next([=](const Data::Session::IdChange &change) { + if (FullMsgId(change.newId.peer, change.oldId) == id) { + lifetime->destroy(); + callback(true); + } + }, *lifetime); + + check(); +} + +void AttachWebView::botInvokeCustomMethod( + Ui::BotWebView::CustomMethodRequest request) { + const auto callback = request.callback; + _bot->session().api().request(MTPbots_InvokeWebViewCustomMethod( + _bot->inputUser, + MTP_string(request.method), + MTP_dataJSON(MTP_bytes(request.params)) + )).done([=](const MTPDataJSON &result) { + callback(result.data().vdata().v); + }).fail([=](const MTP::Error &error) { + callback(base::make_unexpected(error.type())); + }).send(); +} + +void AttachWebView::botClose() { + crl::on_main(this, [=] { cancel(); }); +} + AttachWebView::Context AttachWebView::LookupContext( not_null controller, const Api::SendAction &action) { @@ -568,7 +789,7 @@ void AttachWebView::cancel() { ActiveWebViews().remove(this); _session->api().request(base::take(_requestId)).cancel(); _session->api().request(base::take(_prolongId)).cancel(); - _panel = nullptr; + base::take(_panel); _lastShownContext = base::take(_context); _bot = nullptr; _app = nullptr; @@ -723,6 +944,8 @@ std::optional AttachWebView::lookupLastAction( } void AttachWebView::resolve() { + Expects(!_panel); + resolveUsername(_botUsername, [=](not_null bot) { if (!_context) { return; @@ -1004,98 +1227,6 @@ void AttachWebView::show( bool allowClipboardRead) { Expects(_bot != nullptr && _context != nullptr); - const auto close = crl::guard(this, [=] { - crl::on_main(this, [=] { cancel(); }); - }); - const auto sendData = crl::guard(this, [=](QByteArray data) { - if (!_context - || _context->fromSwitch - || _context->fromBotApp - || _context->action.history->peer != _bot - || queryId) { - return; - } - const auto randomId = base::RandomValue(); - _session->api().request(MTPmessages_SendWebViewData( - _bot->inputUser, - MTP_long(randomId), - MTP_string(buttonText), - MTP_bytes(data) - )).done([=](const MTPUpdates &result) { - _session->api().applyUpdates(result); - }).send(); - crl::on_main(this, [=] { cancel(); }); - }); - const auto switchInlineQuery = crl::guard(this, [=]( - std::vector typeNames, - QString query) { - const auto controller = _context - ? _context->controller.get() - : nullptr; - const auto types = PeerTypesFromNames(typeNames); - if (!_bot - || !_bot->isBot() - || _bot->botInfo->inlinePlaceholder.isEmpty() - || !controller) { - return; - } else if (!types) { - if (_context->dialogsEntryState.key.owningHistory()) { - controller->switchInlineQuery( - _context->dialogsEntryState, - _bot, - query); - } - } else { - const auto bot = _bot; - const auto done = [=](not_null thread) { - controller->switchInlineQuery(thread, bot, query); - }; - ShowChooseBox( - controller, - types, - done, - tr::lng_inline_switch_choose()); - } - crl::on_main(this, [=] { cancel(); }); - }); - const auto handleLocalUri = [close, url](QString uri) { - const auto local = Core::TryConvertUrlToLocal(uri); - if (uri == local || Core::InternalPassportLink(local)) { - return local.startsWith(u"tg://"_q); - } else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive)) { - return false; - } - close(); - crl::on_main([=] { - const auto variant = QVariant::fromValue(ClickHandlerContext{ - .attachBotWebviewUrl = url, - }); - UrlClickHandler::Open(local, variant); - }); - return true; - }; - const auto panel = std::make_shared< - base::weak_ptr>(nullptr); - const auto handleInvoice = [=, session = _session](QString slug) { - using Result = Payments::CheckoutResult; - const auto reactivate = [=](Result result) { - if (const auto strong = panel->get()) { - strong->invoiceClosed(slug, [&] { - switch (result) { - case Result::Paid: return "paid"; - case Result::Failed: return "failed"; - case Result::Pending: return "pending"; - case Result::Cancelled: return "cancelled"; - } - Unexpected("Payments::CheckoutResult value."); - }()); - } - }; - if (const auto strong = panel->get()) { - strong->hideForPayment(); - } - Payments::CheckoutProcess::Start(session, slug, reactivate); - }; auto title = Info::Profile::NameValue(_bot); ActiveWebViews().emplace(this); @@ -1104,9 +1235,6 @@ void AttachWebView::show( _attachBots, not_null{ _bot }, &AttachWebViewBot::user); - const auto name = (attached != end(_attachBots)) - ? attached->name - : _bot->name(); const auto hasSettings = (attached != end(_attachBots)) && !attached->inactive && attached->hasSettings; @@ -1119,59 +1247,19 @@ void AttachWebView::show( | (hasRemoveFromMenu ? Button::RemoveFromMenu : Button::None); const auto bot = _bot; - const auto handleMenuButton = crl::guard(this, [=](Button button) { - switch (button) { - case Button::OpenBot: - close(); - if (bot->session().windows().empty()) { - Core::App().domain().activate(&bot->session().account()); - } - if (!bot->session().windows().empty()) { - const auto window = bot->session().windows().front(); - window->showPeerHistory(bot); - window->window().activate(); - } - break; - case Button::RemoveFromMenu: - if (const auto strong = panel->get()) { - const auto done = crl::guard(this, [=] { - removeFromMenu(bot); - close(); - if (const auto active = Core::App().activeWindow()) { - active->activate(); - } - }); - strong->showBox(Ui::MakeConfirmBox({ - tr::lng_bot_remove_from_menu_sure( - tr::now, - lt_bot, - Ui::Text::Bold(name), - Ui::Text::WithEntities), - done, - })); - } - break; - } - }); - _lastShownUrl = url; + _lastShownQueryId = queryId; + _lastShownButtonText = buttonText; + base::take(_panel); _panel = Ui::BotWebView::Show({ .url = url, .userDataPath = _session->domain().local().webviewDataPath(), .title = std::move(title), .bottom = rpl::single('@' + _bot->username()), - .handleLocalUri = handleLocalUri, - .handleInvoice = handleInvoice, - .sendData = sendData, - .switchInlineQuery = switchInlineQuery, - .close = close, - .phone = _session->user()->phone(), + .delegate = static_cast(this), .menuButtons = buttons, - .handleMenuButton = handleMenuButton, - .themeParams = [] { return Window::Theme::WebViewParams(); }, .allowClipboardRead = allowClipboardRead, }); - *panel = _panel.get(); started(queryId); } diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 0cbd71ab4..a6c254ef8 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -7,9 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "mtproto/sender.h" #include "base/weak_ptr.h" #include "base/flags.h" +#include "mtproto/sender.h" +#include "ui/chat/attach/attach_bot_webview.h" namespace Api { struct SendAction; @@ -64,7 +65,9 @@ struct AttachWebViewBot { bool requestWriteAccess = false; }; -class AttachWebView final : public base::has_weak_ptr { +class AttachWebView final + : public base::has_weak_ptr + , public Ui::BotWebView::Delegate { public: explicit AttachWebView(not_null session); ~AttachWebView(); @@ -130,6 +133,21 @@ public: private: struct Context; + Webview::ThemeParams botThemeParams() override; + bool botHandleLocalUri(QString uri) override; + void botHandleInvoice(QString slug) override; + void botHandleMenuButton(Ui::BotWebView::MenuButton button) override; + void botSendData(QByteArray data) override; + void botSwitchInlineQuery( + std::vector chatTypes, + QString query) override; + void botCheckWriteAccess(Fn callback) override; + void botAllowWriteAccess(Fn callback) override; + void botSharePhone(Fn callback) override; + void botInvokeCustomMethod( + Ui::BotWebView::CustomMethodRequest request) override; + void botClose() override; + [[nodiscard]] static Context LookupContext( not_null controller, const Api::SendAction &action); @@ -185,6 +203,8 @@ private: std::unique_ptr _context; std::unique_ptr _lastShownContext; QString _lastShownUrl; + uint64 _lastShownQueryId = 0; + QString _lastShownButtonText; UserData *_bot = nullptr; QString _botUsername; QString _botAppName; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 1930c5484..f25340450 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -315,25 +315,12 @@ Panel::Progress::Progress(QWidget *parent, Fn rect) Panel::Panel( const QString &userDataPath, rpl::producer title, - Fn handleLocalUri, - Fn handleInvoice, - Fn sendData, - Fn, QString)> switchInlineQuery, - Fn close, - QString phone, + not_null delegate, MenuButtons menuButtons, - Fn handleMenuButton, - Fn themeParams, bool allowClipboardRead) : _userDataPath(userDataPath) -, _handleLocalUri(std::move(handleLocalUri)) -, _handleInvoice(std::move(handleInvoice)) -, _sendData(std::move(sendData)) -, _switchInlineQuery(std::move(switchInlineQuery)) -, _close(std::move(close)) -, _phone(phone) +, _delegate(delegate) , _menuButtons(menuButtons) -, _handleMenuButton(std::move(handleMenuButton)) , _widget(std::make_unique()) , _allowClipboardRead(allowClipboardRead) { _widget->setInnerSize(st::paymentsPanelSize); @@ -344,14 +331,16 @@ Panel::Panel( if (_closeNeedConfirmation) { scheduleCloseWithConfirmation(); } else { - _close(); + _delegate->botClose(); } }, _widget->lifetime()); _widget->closeEvents( ) | rpl::filter([=] { return !_hiddenForPayment; - }) | rpl::start_with_next(_close, _widget->lifetime()); + }) | rpl::start_with_next([=] { + _delegate->botClose(); + }, _widget->lifetime()); _widget->backRequests( ) | rpl::start_with_next([=] { @@ -367,7 +356,7 @@ Panel::Panel( _themeUpdateScheduled = true; crl::on_main(_widget.get(), [=] { _themeUpdateScheduled = false; - updateThemeParams(themeParams()); + updateThemeParams(_delegate->botThemeParams()); }); }, _widget->lifetime()); @@ -540,7 +529,7 @@ bool Panel::showWebview( } if (_menuButtons & MenuButton::OpenBot) { callback(tr::lng_bot_open(tr::now), [=] { - _handleMenuButton(MenuButton::OpenBot); + _delegate->botHandleMenuButton(MenuButton::OpenBot); }, &st::menuIconLeave); } callback(tr::lng_bot_reload_page(tr::now), [=] { @@ -548,7 +537,7 @@ bool Panel::showWebview( }, &st::menuIconRestore); if (_menuButtons & MenuButton::RemoveFromMenu) { const auto handler = [=] { - _handleMenuButton(MenuButton::RemoveFromMenu); + _delegate->botHandleMenuButton(MenuButton::RemoveFromMenu); }; callback({ .text = tr::lng_bot_remove_from_menu(tr::now), @@ -624,7 +613,7 @@ bool Panel::createWebview() { const auto command = list.at(0).toString(); const auto arguments = ParseMethodArgs(list.at(1).toString()); if (command == "web_app_close") { - _close(); + _delegate->botClose(); } else if (command == "web_app_data_send") { sendDataMessage(arguments); } else if (command == "web_app_switch_inline_query") { @@ -645,8 +634,12 @@ bool Panel::createWebview() { openInvoice(arguments); } else if (command == "web_app_open_popup") { openPopup(arguments); + } else if (command == "web_app_request_write_access") { + requestWriteAccess(); } else if (command == "web_app_request_phone") { requestPhone(); + } else if (command == "web_app_invoke_custom_method") { + invokeCustomMethod(arguments); } else if (command == "web_app_setup_closing_behavior") { setupClosingBehaviour(arguments); } else if (command == "web_app_read_text_from_clipboard") { @@ -655,7 +648,7 @@ bool Panel::createWebview() { }); raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) { - if (_handleLocalUri(uri)) { + if (_delegate->botHandleLocalUri(uri)) { return false; } else if (newWindow) { return true; @@ -694,27 +687,27 @@ void Panel::setTitle(rpl::producer title) { void Panel::sendDataMessage(const QJsonObject &args) { if (args.isEmpty()) { - _close(); + _delegate->botClose(); return; } const auto data = args["data"].toString(); if (data.isEmpty()) { LOG(("BotWebView Error: Bad 'data' in sendDataMessage.")); - _close(); + _delegate->botClose(); return; } - _sendData(data.toUtf8()); + _delegate->botSendData(data.toUtf8()); } void Panel::switchInlineQueryMessage(const QJsonObject &args) { if (args.isEmpty()) { - _close(); + _delegate->botClose(); return; } const auto query = args["query"].toString(); if (query.isEmpty()) { LOG(("BotWebView Error: Bad 'query' in switchInlineQueryMessage.")); - _close(); + _delegate->botClose(); return; } const auto valid = base::flat_set{ @@ -735,26 +728,26 @@ void Panel::switchInlineQueryMessage(const QJsonObject &args) { break; } } - _switchInlineQuery(types, query); + _delegate->botSwitchInlineQuery(types, query); } void Panel::openTgLink(const QJsonObject &args) { if (args.isEmpty()) { - _close(); + _delegate->botClose(); return; } const auto path = args["path_full"].toString(); if (path.isEmpty()) { LOG(("BotWebView Error: Bad 'path_full' in openTgLink.")); - _close(); + _delegate->botClose(); return; } - _handleLocalUri("https://t.me" + path); + _delegate->botHandleLocalUri("https://t.me" + path); } void Panel::openExternalLink(const QJsonObject &args) { if (args.isEmpty()) { - _close(); + _delegate->botClose(); return; } const auto url = args["url"].toString(); @@ -762,7 +755,7 @@ void Panel::openExternalLink(const QJsonObject &args) { if (url.isEmpty() || (!lower.startsWith("http://") && !lower.startsWith("https://"))) { LOG(("BotWebView Error: Bad 'url' in openExternalLink.")); - _close(); + _delegate->botClose(); return; } else if (!allowOpenLink()) { return; @@ -772,21 +765,21 @@ void Panel::openExternalLink(const QJsonObject &args) { void Panel::openInvoice(const QJsonObject &args) { if (args.isEmpty()) { - _close(); + _delegate->botClose(); return; } const auto slug = args["slug"].toString(); if (slug.isEmpty()) { LOG(("BotWebView Error: Bad 'slug' in openInvoice.")); - _close(); + _delegate->botClose(); return; } - _handleInvoice(slug); + _delegate->botHandleInvoice(slug); } void Panel::openPopup(const QJsonObject &args) { if (args.isEmpty()) { - _close(); + _delegate->botClose(); return; } using Button = Webview::PopupArgs::Button; @@ -805,7 +798,7 @@ void Panel::openPopup(const QJsonObject &args) { const auto i = types.find(fields["type"].toString()); if (i == end(types)) { LOG(("BotWebView Error: Bad 'type' in openPopup buttons.")); - _close(); + _delegate->botClose(); return; } buttons.push_back({ @@ -816,11 +809,11 @@ void Panel::openPopup(const QJsonObject &args) { } if (message.isEmpty()) { LOG(("BotWebView Error: Bad 'message' in openPopup.")); - _close(); + _delegate->botClose(); return; } else if (buttons.empty()) { LOG(("BotWebView Error: Bad 'buttons' in openPopup.")); - _close(); + _delegate->botClose(); return; } const auto widget = _webview->window.widget(); @@ -838,8 +831,65 @@ void Panel::openPopup(const QJsonObject &args) { } } +void Panel::requestWriteAccess() { + if (_inBlockingRequest) { + replyRequestWriteAccess(false); + return; + } + _inBlockingRequest = true; + const auto finish = [=](bool allowed) { + _inBlockingRequest = false; + replyRequestWriteAccess(allowed); + }; + const auto weak = base::make_weak(this); + _delegate->botCheckWriteAccess([=](bool allowed) { + if (!weak) { + return; + } else if (allowed) { + finish(true); + return; + } + using Button = Webview::PopupArgs::Button; + const auto widget = _webview->window.widget(); + const auto integration = &Ui::Integration::Instance(); + const auto result = Webview::ShowBlockingPopup({ + .parent = widget ? widget->window() : nullptr, + .title = integration->phraseBotAllowWriteTitle(), + .text = integration->phraseBotAllowWrite(), + .buttons = { + { + .id = "allow", + .text = integration->phraseBotAllowWriteConfirm(), + }, + { .id = "cancel", .type = Button::Type::Cancel }, + }, + }); + if (!weak) { + return; + } else if (result.id == "allow") { + _delegate->botAllowWriteAccess(crl::guard(this, finish)); + } else { + finish(false); + } + }); +} + +void Panel::replyRequestWriteAccess(bool allowed) { + postEvent("write_access_requested", QJsonObject{ + { u"status"_q, allowed ? u"allowed"_q : u"cancelled"_q } + }); +} + void Panel::requestPhone() { -#if 0 // disabled for now + if (_inBlockingRequest) { + replyRequestPhone(false); + return; + } + _inBlockingRequest = true; + const auto finish = [=](bool shared) { + _inBlockingRequest = false; + replyRequestPhone(shared); + }; using Button = Webview::PopupArgs::Button; const auto widget = _webview->window.widget(); const auto weak = base::make_weak(this); @@ -853,15 +903,62 @@ void Panel::requestPhone() { .id = "share", .text = integration->phraseBotSharePhoneConfirm(), }, - {.id = "cancel", .type = Button::Type::Cancel }, + { .id = "cancel", .type = Button::Type::Cancel }, }, }); - if (weak) { - postEvent("phone_requested", (result.id == "share") - ? QJsonObject{ { u"phone_number"_q, _phone } } - : EventData()); + if (!weak) { + return; + } else if (result.id == "share") { + _delegate->botSharePhone(crl::guard(this, finish)); + } else { + finish(false); } -#endif +} + +void Panel::replyRequestPhone(bool shared) { + postEvent("phone_requested", QJsonObject{ + { u"status"_q, shared ? u"sent"_q : u"cancelled"_q } + }); +} + +void Panel::invokeCustomMethod(const QJsonObject &args) { + const auto requestId = args["req_id"]; + if (requestId.isUndefined()) { + return; + } + const auto finish = [=](QJsonObject response) { + replyCustomMethod(requestId, std::move(response)); + }; + auto callback = crl::guard(this, [=](CustomMethodResult result) { + if (result) { + auto error = QJsonParseError(); + const auto parsed = QJsonDocument::fromJson( + "{ \"result\": " + *result + '}', + &error); + if (error.error != QJsonParseError::NoError + || !parsed.isObject() + || parsed.object().size() != 1) { + finish({ { u"error"_q, u"Could not parse response."_q } }); + } else { + finish(parsed.object()); + } + } else { + finish({ { u"error"_q, result.error() } }); + } + }); + const auto params = QJsonDocument( + args["params"].toObject() + ).toJson(QJsonDocument::Compact); + _delegate->botInvokeCustomMethod({ + .method = args["method"].toString(), + .params = params, + .callback = std::move(callback), + }); +} + +void Panel::replyCustomMethod(QJsonValue requestId, QJsonObject response) { + response["req_id"] = requestId; + postEvent(u"custom_method_invoked"_q, response); } void Panel::requestClipboardText(const QJsonObject &args) { @@ -929,7 +1026,7 @@ void Panel::closeWithConfirmation() { if (!weak) { return; } else if (result.id != "cancel") { - _close(); + _delegate->botClose(); } else { _closeWithConfirmationScheduled = false; } @@ -941,7 +1038,7 @@ void Panel::setupClosingBehaviour(const QJsonObject &args) { void Panel::processMainButtonMessage(const QJsonObject &args) { if (args.isEmpty()) { - _close(); + _delegate->botClose(); return; } @@ -1158,20 +1255,13 @@ rpl::lifetime &Panel::lifetime() { } std::unique_ptr Show(Args &&args) { - const auto params = args.themeParams(); auto result = std::make_unique( args.userDataPath, std::move(args.title), - std::move(args.handleLocalUri), - std::move(args.handleInvoice), - std::move(args.sendData), - std::move(args.switchInlineQuery), - std::move(args.close), - args.phone, + args.delegate, args.menuButtons, - std::move(args.handleMenuButton), - std::move(args.themeParams), args.allowClipboardRead); + const auto params = args.delegate->botThemeParams(); if (!result->showWebview(args.url, params, std::move(args.bottom))) { const auto available = Webview::Availability(); if (available.error != Webview::Available::Error::None) { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index c2898afc2..54bf92aa9 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -7,10 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/expected.h" #include "base/object_ptr.h" #include "base/weak_ptr.h" #include "base/flags.h" +class QJsonObject; +class QJsonValue; + namespace Ui { class BoxContent; class RpWidget; @@ -40,20 +44,37 @@ enum class MenuButton { inline constexpr bool is_flag_type(MenuButton) { return true; } using MenuButtons = base::flags; +using CustomMethodResult = base::expected; +struct CustomMethodRequest { + QString method; + QByteArray params; + Fn callback; +}; + +class Delegate { +public: + virtual Webview::ThemeParams botThemeParams() = 0; + virtual bool botHandleLocalUri(QString uri) = 0; + virtual void botHandleInvoice(QString slug) = 0; + virtual void botHandleMenuButton(MenuButton button) = 0; + virtual void botSendData(QByteArray data) = 0; + virtual void botSwitchInlineQuery( + std::vector chatTypes, + QString query) = 0; + virtual void botCheckWriteAccess(Fn callback) = 0; + virtual void botAllowWriteAccess(Fn callback) = 0; + virtual void botSharePhone(Fn callback) = 0; + virtual void botInvokeCustomMethod(CustomMethodRequest request) = 0; + virtual void botClose() = 0; +}; + class Panel final : public base::has_weak_ptr { public: Panel( const QString &userDataPath, rpl::producer title, - Fn handleLocalUri, - Fn handleInvoice, - Fn sendData, - Fn, QString)> switchInlineQuery, - Fn close, - QString phone, + not_null delegate, MenuButtons menuButtons, - Fn handleMenuButton, - Fn themeParams, bool allowClipboardRead); ~Panel(); @@ -96,7 +117,12 @@ private: void openExternalLink(const QJsonObject &args); void openInvoice(const QJsonObject &args); void openPopup(const QJsonObject &args); + void requestWriteAccess(); + void replyRequestWriteAccess(bool allowed); void requestPhone(); + void replyRequestPhone(bool shared); + void invokeCustomMethod(const QJsonObject &args); + void replyCustomMethod(QJsonValue requestId, QJsonObject response); void requestClipboardText(const QJsonObject &args); void setupClosingBehaviour(const QJsonObject &args); void createMainButton(); @@ -115,15 +141,9 @@ private: void setupProgressGeometry(); QString _userDataPath; - Fn _handleLocalUri; - Fn _handleInvoice; - Fn _sendData; - Fn, QString)> _switchInlineQuery; - Fn _close; - QString _phone; + const not_null _delegate; bool _closeNeedConfirmation = false; MenuButtons _menuButtons = {}; - Fn _handleMenuButton; std::unique_ptr _widget; std::unique_ptr _webview; std::unique_ptr _webviewBottom; @@ -139,6 +159,7 @@ private: bool _hiddenForPayment = false; bool _closeWithConfirmationScheduled = false; bool _allowClipboardRead = false; + bool _inBlockingRequest = false; }; @@ -147,15 +168,8 @@ struct Args { QString userDataPath; rpl::producer title; rpl::producer bottom; - Fn handleLocalUri; - Fn handleInvoice; - Fn sendData; - Fn, QString)> switchInlineQuery; - Fn close; - QString phone; + not_null delegate; MenuButtons menuButtons; - Fn handleMenuButton; - Fn themeParams; bool allowClipboardRead = false; }; [[nodiscard]] std::unique_ptr Show(Args &&args);