/* 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 "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" #include "data/data_document_media.h" #include "data/data_session.h" #include "main/main_session.h" #include "main/main_domain.h" #include "storage/storage_domain.h" #include "info/profile/info_profile_values.h" #include "ui/boxes/confirm_box.h" #include "ui/toasts/common_toasts.h" #include "ui/chat/attach/attach_bot_webview.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/dropdown_menu.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/menu/menu_item_base.h" #include "ui/text/text_utilities.h" #include "ui/effects/ripple_animation.h" #include "ui/painter.h" #include "window/themes/window_theme.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "webview/webview_interface.h" #include "core/application.h" #include "core/local_url_handlers.h" #include "ui/basic_click_handlers.h" #include "history/history.h" #include "history/history_item.h" #include "payments/payments_checkout_process.h" #include "storage/storage_account.h" #include "boxes/peer_list_controllers.h" #include "lang/lang_keys.h" #include "base/random.h" #include "base/timer_rpl.h" #include "apiwrap.h" #include "mainwidget.h" #include "styles/style_boxes.h" #include "styles/style_menu_icons.h" #include namespace InlineBots { namespace { constexpr auto kProlongTimeout = 60 * crl::time(1000); struct ParsedBot { UserData *bot = nullptr; bool inactive = false; }; [[nodiscard]] DocumentData *ResolveIcon( not_null session, const MTPDattachMenuBot &data) { for (const auto &icon : data.vicons().v) { const auto document = icon.match([&]( const MTPDattachMenuBotIcon &data ) -> DocumentData* { if (data.vname().v == "default_static") { return session->data().processDocument(data.vicon()).get(); } return nullptr; }); if (document) { return document; } } return nullptr; } [[nodiscard]] PeerTypes ResolvePeerTypes( const QVector &types) { auto result = PeerTypes(); for (const auto &type : types) { result |= type.match([&](const MTPDattachMenuPeerTypeSameBotPM &) { return PeerType::SameBot; }, [&](const MTPDattachMenuPeerTypeBotPM &) { return PeerType::Bot; }, [&](const MTPDattachMenuPeerTypePM &) { return PeerType::User; }, [&](const MTPDattachMenuPeerTypeChat &) { return PeerType::Group; }, [&](const MTPDattachMenuPeerTypeBroadcast &) { return PeerType::Broadcast; }); } return result; } [[nodiscard]] std::optional ParseAttachBot( not_null session, const MTPAttachMenuBot &bot) { auto result = bot.match([&](const MTPDattachMenuBot &data) { const auto user = session->data().userLoaded(UserId(data.vbot_id())); const auto good = user && user->isBot() && user->botInfo->supportsAttachMenu; return good ? AttachWebViewBot{ .user = user, .icon = ResolveIcon(session, data), .name = qs(data.vshort_name()), .types = ResolvePeerTypes(data.vpeer_types().v), .inactive = data.is_inactive(), .hasSettings = data.is_has_settings(), .requestWriteAccess = data.is_request_write_access(), } : std::optional(); }); if (result && result->icon) { result->icon->forceToCache(true); } return result; } [[nodiscard]] PeerTypes PeerTypesFromNames( const std::vector &names) { auto result = PeerTypes(); for (const auto &name : names) { //, bots, groups, channels result |= (name == u"users"_q) ? PeerType::User : name == u"bots"_q ? PeerType::Bot : name == u"groups"_q ? PeerType::Group : name == u"channels"_q ? PeerType::Broadcast : PeerType(0); } return result; } void ShowChooseBox( not_null controller, PeerTypes types, Fn)> callback, rpl::producer titleOverride = nullptr) { const auto weak = std::make_shared>(); auto done = [=](not_null thread) mutable { if (const auto strong = *weak) { strong->closeBox(); } callback(thread); }; auto filter = [=](not_null thread) -> bool { const auto peer = thread->peer(); if (!Data::CanSend(thread, ChatRestriction::SendInline, false)) { return false; } else if (const auto user = peer->asUser()) { if (user->isBot()) { return (types & PeerType::Bot); } else { return (types & PeerType::User); } } else if (peer->isBroadcast()) { return (types & PeerType::Broadcast); } else { return (types & PeerType::Group); } }; auto initBox = [=](not_null box) { if (titleOverride) { box->setTitle(std::move(titleOverride)); } box->addButton(tr::lng_cancel(), [box] { box->closeBox(); }); }; *weak = controller->show(Box( std::make_unique( &controller->session(), std::move(done), std::move(filter)), std::move(initBox)), Ui::LayerOption::KeepOther); } [[nodiscard]] base::flat_set> &ActiveWebViews() { static auto result = base::flat_set>(); return result; } class BotAction final : public Ui::Menu::ItemBase { public: BotAction( not_null parent, const style::Menu &st, const AttachWebViewBot &bot, Fn callback); bool isEnabled() const override; not_null action() const override; [[nodiscard]] rpl::producer forceShown() const; void handleKeyPress(not_null e) override; private: void contextMenuEvent(QContextMenuEvent *e) override; QPoint prepareRippleStartPosition() const override; QImage prepareRippleMask() const override; int contentHeight() const override; void prepare(); void validateIcon(); void paint(Painter &p); const not_null _dummyAction; const style::Menu &_st; const AttachWebViewBot _bot; base::unique_qptr _menu; rpl::event_stream _forceShown; Ui::Text::String _text; QImage _mask; QImage _icon; int _textWidth = 0; const int _height; }; BotAction::BotAction( not_null parent, const style::Menu &st, const AttachWebViewBot &bot, Fn callback) : ItemBase(parent, st) , _dummyAction(new QAction(parent)) , _st(st) , _bot(bot) , _height(_st.itemPadding.top() + _st.itemStyle.font->height + _st.itemPadding.bottom()) { setAcceptBoth(false); initResizeHook(parent->sizeValue()); setClickedCallback(std::move(callback)); paintRequest( ) | rpl::start_with_next([=] { Painter p(this); paint(p); }, lifetime()); style::PaletteChanged( ) | rpl::start_with_next([=] { _icon = QImage(); update(); }, lifetime()); enableMouseSelecting(); prepare(); } void BotAction::validateIcon() { if (_mask.isNull()) { if (!_bot.media || !_bot.media->loaded()) { return; } auto icon = QSvgRenderer(_bot.media->bytes()); if (!icon.isValid()) { _mask = QImage( QSize(1, 1) * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied); _mask.fill(Qt::transparent); } else { const auto size = style::ConvertScale(icon.defaultSize()); _mask = QImage( size * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied); _mask.setDevicePixelRatio(style::DevicePixelRatio()); _mask.fill(Qt::transparent); { auto p = QPainter(&_mask); icon.render(&p, QRect(QPoint(), size)); } _mask = Images::Colored(std::move(_mask), QColor(255, 255, 255)); } } if (_icon.isNull()) { _icon = style::colorizeImage(_mask, st::menuIconColor); } } void BotAction::paint(Painter &p) { validateIcon(); const auto selected = isSelected(); if (selected && _st.itemBgOver->c.alpha() < 255) { p.fillRect(0, 0, width(), _height, _st.itemBg); } p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg); if (isEnabled()) { paintRipple(p, 0, 0); } if (!_icon.isNull()) { p.drawImage(_st.itemIconPosition, _icon); } p.setPen(selected ? _st.itemFgOver : _st.itemFg); _text.drawLeftElided( p, _st.itemPadding.left(), _st.itemPadding.top(), _textWidth, width()); } void BotAction::prepare() { _text.setMarkedText(_st.itemStyle, { _bot.name }); const auto textWidth = _text.maxWidth(); const auto &padding = _st.itemPadding; const auto goodWidth = padding.left() + textWidth + padding.right(); const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax); _textWidth = w - (goodWidth - textWidth); setMinWidth(w); update(); } bool BotAction::isEnabled() const { return true; } not_null BotAction::action() const { return _dummyAction; } void BotAction::contextMenuEvent(QContextMenuEvent *e) { _menu = nullptr; _menu = base::make_unique_q( this, st::popupMenuWithIcons); _menu->addAction(tr::lng_bot_remove_from_menu(tr::now), [=] { _bot.user->session().attachWebView().removeFromMenu(_bot.user); }, &st::menuIconDelete); QObject::connect(_menu, &QObject::destroyed, [=] { _forceShown.fire(false); }); _forceShown.fire(true); _menu->popup(e->globalPos()); e->accept(); } QPoint BotAction::prepareRippleStartPosition() const { return mapFromGlobal(QCursor::pos()); } QImage BotAction::prepareRippleMask() const { return Ui::RippleAnimation::RectMask(size()); } int BotAction::contentHeight() const { return _height; } rpl::producer BotAction::forceShown() const { return _forceShown.events(); } void BotAction::handleKeyPress(not_null e) { if (!isSelected()) { return; } const auto key = e->key(); if (key == Qt::Key_Enter || key == Qt::Key_Return) { setClicked(Ui::Menu::TriggeredSource::Keyboard); } } } // namespace bool PeerMatchesTypes( not_null peer, not_null bot, PeerTypes types) { if (const auto user = peer->asUser()) { return (user == bot) ? (types & PeerType::SameBot) : user->isBot() ? (types & PeerType::Bot) : (types & PeerType::User); } else if (peer->isBroadcast()) { return (types & PeerType::Broadcast); } return (types & PeerType::Group); } PeerTypes ParseChooseTypes(QStringView choose) { auto result = PeerTypes(); for (const auto &entry : choose.split(QChar(' '))) { if (entry == u"users"_q) { result |= PeerType::User; } else if (entry == u"bots"_q) { result |= PeerType::Bot; } else if (entry == u"groups"_q) { result |= PeerType::Group; } else if (entry == u"channels"_q) { result |= PeerType::Broadcast; } } return result; } struct AttachWebView::Context { base::weak_ptr controller; Dialogs::EntryState dialogsEntryState; Api::SendAction action; bool fromSwitch = false; bool fromBotApp = false; }; AttachWebView::AttachWebView(not_null session) : _session(session) { } AttachWebView::~AttachWebView() { ActiveWebViews().remove(this); } void AttachWebView::request( not_null controller, const Api::SendAction &action, const QString &botUsername, const QString &startCommand) { if (botUsername.isEmpty()) { return; } const auto username = _bot ? _bot->username() : _botUsername; const auto context = LookupContext(controller, action); if (IsSame(_context, context) && username.toLower() == botUsername.toLower() && _startCommand == startCommand) { if (_panel) { _panel->requestActivate(); } return; } cancel(); _context = std::make_unique(context); _botUsername = botUsername; _startCommand = startCommand; resolve(); } AttachWebView::Context AttachWebView::LookupContext( not_null controller, const Api::SendAction &action) { return { .controller = controller, .dialogsEntryState = controller->currentDialogsEntryState(), .action = action, }; } bool AttachWebView::IsSame( const std::unique_ptr &a, const Context &b) { // Check fields that are sent to API in bot attach webview requests. return a && (a->controller == b.controller) && (a->dialogsEntryState == b.dialogsEntryState) && (a->fromSwitch == b.fromSwitch) && (a->action.history == b.action.history) && (a->action.replyTo == b.action.replyTo) && (a->action.topicRootId == b.action.topicRootId) && (a->action.options.sendAs == b.action.options.sendAs) && (a->action.options.silent == b.action.options.silent); } void AttachWebView::request( not_null controller, const Api::SendAction &action, not_null bot, const WebViewButton &button) { requestWithOptionalConfirm( bot, button, LookupContext(controller, action), button.fromMenu ? nullptr : controller.get()); } void AttachWebView::requestWithOptionalConfirm( not_null bot, const WebViewButton &button, const Context &context, Window::SessionController *controllerForConfirm) { if (IsSame(_context, context) && _bot == bot) { if (_panel) { _panel->requestActivate(); } else if (_requestId) { return; } } cancel(); _bot = bot; _context = std::make_unique(context); if (controllerForConfirm) { confirmOpen(controllerForConfirm, [=] { request(button); }); } else { request(button); } } void AttachWebView::request(const WebViewButton &button) { Expects(_context != nullptr && _bot != nullptr); _startCommand = button.startCommand; const auto &action = _context->action; using Flag = MTPmessages_RequestWebView::Flag; const auto flags = Flag::f_theme_params | (button.url.isEmpty() ? Flag(0) : Flag::f_url) | (_startCommand.isEmpty() ? Flag(0) : Flag::f_start_param) | (action.replyTo ? Flag::f_reply_to_msg_id : Flag(0)) | (action.topicRootId ? Flag::f_top_msg_id : Flag(0)) | (action.options.sendAs ? Flag::f_send_as : Flag(0)) | (action.options.silent ? Flag::f_silent : Flag(0)); _requestId = _session->api().request(MTPmessages_RequestWebView( MTP_flags(flags), action.history->peer->input, _bot->inputUser, MTP_bytes(button.url), MTP_string(_startCommand), MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), MTP_string("tdesktop"), MTP_int(action.replyTo.bare), MTP_int(action.topicRootId.bare), (action.options.sendAs ? action.options.sendAs->input : MTP_inputPeerEmpty()) )).done([=](const MTPWebViewResult &result) { _requestId = 0; 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) { requestBots(); } }).send(); } void AttachWebView::cancel() { ActiveWebViews().remove(this); _session->api().request(base::take(_requestId)).cancel(); _session->api().request(base::take(_prolongId)).cancel(); _panel = nullptr; _context = nullptr; _bot = nullptr; _app = nullptr; _botUsername = QString(); _botAppName = QString(); _startCommand = QString(); } void AttachWebView::requestBots() { if (_botsRequestId) { return; } _botsRequestId = _session->api().request(MTPmessages_GetAttachMenuBots( MTP_long(_botsHash) )).done([=](const MTPAttachMenuBots &result) { _botsRequestId = 0; result.match([&](const MTPDattachMenuBotsNotModified &) { }, [&](const MTPDattachMenuBots &data) { _session->data().processUsers(data.vusers()); _botsHash = data.vhash().v; _attachBots.clear(); _attachBots.reserve(data.vbots().v.size()); for (const auto &bot : data.vbots().v) { if (auto parsed = ParseAttachBot(_session, bot)) { if (!parsed->inactive) { if (const auto icon = parsed->icon) { parsed->media = icon->createMediaView(); icon->save(Data::FileOrigin(), {}); } _attachBots.push_back(std::move(*parsed)); } } } _attachBotsUpdates.fire({}); }); }).fail([=] { _botsRequestId = 0; }).send(); } void AttachWebView::requestAddToMenu( not_null bot, const QString &startCommand) { requestAddToMenu(bot, startCommand, nullptr, std::nullopt, PeerTypes()); } void AttachWebView::requestAddToMenu( not_null bot, const QString &startCommand, Window::SessionController *controller, std::optional action, PeerTypes chooseTypes) { Expects(controller != nullptr || _context != nullptr); if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) { showToast(tr::lng_bot_menu_not_supported(tr::now), controller); return; } const auto wasController = (controller != nullptr); _addToMenuChooseController = base::make_weak(controller); _addToMenuStartCommand = startCommand; _addToMenuChooseTypes = chooseTypes; if (!controller) { _addToMenuContext = base::take(_context); } else if (action) { _addToMenuContext = std::make_unique( LookupContext(controller, *action)); } if (_addToMenuId) { if (_addToMenuBot == bot) { return; } _session->api().request(base::take(_addToMenuId)).cancel(); } _addToMenuBot = bot; _addToMenuId = _session->api().request(MTPmessages_GetAttachMenuBot( bot->inputUser )).done([=](const MTPAttachMenuBotsBot &result) { _addToMenuId = 0; const auto bot = base::take(_addToMenuBot); const auto context = std::shared_ptr(base::take(_addToMenuContext)); const auto chooseTypes = base::take(_addToMenuChooseTypes); const auto startCommand = base::take(_addToMenuStartCommand); const auto chooseController = base::take(_addToMenuChooseController); const auto open = [=](PeerTypes types) { const auto strong = chooseController.get(); if (!strong) { if (wasController) { // Just ignore the click if controller was destroyed. return true; } } else if (const auto useTypes = chooseTypes & types) { const auto done = [=](not_null thread) { strong->showThread(thread); requestWithOptionalConfirm( bot, { .startCommand = startCommand }, LookupContext(strong, Api::SendAction(thread))); }; ShowChooseBox(strong, useTypes, done); return true; } if (!context) { return false; } requestWithOptionalConfirm( bot, { .startCommand = startCommand }, *context); return true; }; result.match([&](const MTPDattachMenuBotsBot &data) { _session->data().processUsers(data.vusers()); if (const auto parsed = ParseAttachBot(_session, data.vbot())) { if (bot == parsed->user) { const auto types = parsed->types; if (parsed->inactive) { confirmAddToMenu(*parsed, [=] { open(types); }); } else { requestBots(); if (!open(types)) { showToast( tr::lng_bot_menu_already_added(tr::now)); } } } } }); }).fail([=] { _addToMenuId = 0; _addToMenuBot = nullptr; _addToMenuContext = nullptr; _addToMenuStartCommand = QString(); showToast(tr::lng_bot_menu_not_supported(tr::now)); }).send(); } void AttachWebView::removeFromMenu(not_null bot) { toggleInMenu(bot, ToggledState::Removed, [=] { showToast(tr::lng_bot_remove_from_menu_done(tr::now)); }); } void AttachWebView::resolve() { resolveUsername(_botUsername, [=](not_null bot) { if (!_context) { return; } _bot = bot->asUser(); if (!_bot) { showToast(tr::lng_bot_menu_not_supported(tr::now)); return; } requestAddToMenu(_bot, _startCommand); }); } void AttachWebView::resolveUsername( const QString &username, Fn)> done) { if (const auto peer = _session->data().peerByUsername(username)) { done(peer); return; } _session->api().request(base::take(_requestId)).cancel(); _requestId = _session->api().request(MTPcontacts_ResolveUsername( MTP_string(username) )).done([=](const MTPcontacts_ResolvedPeer &result) { _requestId = 0; result.match([&](const MTPDcontacts_resolvedPeer &data) { _session->data().processUsers(data.vusers()); _session->data().processChats(data.vchats()); if (const auto peerId = peerFromMTP(data.vpeer())) { done(_session->data().peer(peerId)); } }); }).fail([=](const MTP::Error &error) { _requestId = 0; if (error.code() == 400) { showToast( tr::lng_username_not_found(tr::now, lt_user, username)); } }).send(); } void AttachWebView::requestSimple( not_null controller, not_null bot, const WebViewButton &button) { cancel(); _bot = bot; _context = std::make_unique(LookupContext( controller, Api::SendAction(bot->owner().history(bot)))); _context->fromSwitch = button.fromSwitch; confirmOpen(controller, [=] { requestSimple(button); }); } void AttachWebView::requestSimple(const WebViewButton &button) { using Flag = MTPmessages_RequestSimpleWebView::Flag; _requestId = _session->api().request(MTPmessages_RequestSimpleWebView( MTP_flags(Flag::f_theme_params | (button.fromSwitch ? Flag::f_from_switch_webview : Flag())), _bot->inputUser, MTP_bytes(button.url), MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), MTP_string("tdesktop") )).done([=](const MTPSimpleWebViewResult &result) { _requestId = 0; result.match([&](const MTPDsimpleWebViewResultUrl &data) { const auto queryId = uint64(); show(queryId, qs(data.vurl()), button.text); }); }).fail([=](const MTP::Error &error) { _requestId = 0; }).send(); } void AttachWebView::requestMenu( not_null controller, not_null bot) { cancel(); _bot = bot; _context = std::make_unique(LookupContext( controller, Api::SendAction(bot->owner().history(bot)))); const auto url = bot->botInfo->botMenuButtonUrl; const auto text = bot->botInfo->botMenuButtonText; confirmOpen(controller, [=] { const auto &action = _context->action; using Flag = MTPmessages_RequestWebView::Flag; _requestId = _session->api().request(MTPmessages_RequestWebView( MTP_flags(Flag::f_theme_params | Flag::f_url | Flag::f_from_bot_menu | (action.replyTo? Flag::f_reply_to_msg_id : Flag(0)) | (action.topicRootId ? Flag::f_top_msg_id : Flag(0)) | (action.options.sendAs ? Flag::f_send_as : Flag(0)) | (action.options.silent ? Flag::f_silent : Flag(0))), action.history->peer->input, _bot->inputUser, MTP_string(url), MTPstring(), // start_param MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), MTP_string("tdesktop"), MTP_int(action.replyTo.bare), MTP_int(action.topicRootId.bare), (action.options.sendAs ? action.options.sendAs->input : MTP_inputPeerEmpty()) )).done([=](const MTPWebViewResult &result) { _requestId = 0; 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) { requestBots(); } }).send(); }); } 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) { if (!_bot) { return; } else if (_bot->isVerified() || _bot->session().local().isBotTrustedOpenWebView(_bot->id)) { done(); return; } const auto callback = [=] { _bot->session().local().markBotTrustedOpenWebView(_bot->id); controller->hideLayer(); done(); }; controller->show(Ui::MakeConfirmBox({ .text = tr::lng_allow_bot_webview( tr::now, lt_bot_name, Ui::Text::Bold(_bot->name()), Ui::Text::RichLangValue), .confirmed = callback, .confirmText = tr::lng_box_ok(), })); } void AttachWebView::ClearAll() { while (!ActiveWebViews().empty()) { ActiveWebViews().front()->cancel(); } } void AttachWebView::show( uint64 queryId, const QString &url, const QString &buttonText, 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](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; } UrlClickHandler::Open(local, {}); close(); 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); using Button = Ui::BotWebView::MenuButton; const auto attached = ranges::find( _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; const auto hasOpenBot = !_context || (_bot != _context->action.history->peer); const auto hasRemoveFromMenu = (attached != end(_attachBots)) && !attached->inactive; const auto buttons = (hasSettings ? Button::Settings : Button::None) | (hasOpenBot ? Button::OpenBot : Button::None) | (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; } }); _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(), .menuButtons = buttons, .handleMenuButton = handleMenuButton, .themeParams = [] { return Window::Theme::WebViewParams(); }, .allowClipboardRead = allowClipboardRead, }); *panel = _panel.get(); started(queryId); } void AttachWebView::started(uint64 queryId) { Expects(_bot != nullptr && _context != nullptr); if (_context->fromSwitch || !queryId) { return; } _session->data().webViewResultSent( ) | rpl::filter([=](const Data::Session::WebViewResultSent &sent) { return (sent.queryId == queryId); }) | rpl::start_with_next([=] { cancel(); }, _panel->lifetime()); const auto action = _context->action; base::timer_each( kProlongTimeout ) | rpl::start_with_next([=] { using Flag = MTPmessages_ProlongWebView::Flag; _session->api().request(base::take(_prolongId)).cancel(); _prolongId = _session->api().request(MTPmessages_ProlongWebView( MTP_flags(Flag(0) | (action.replyTo ? Flag::f_reply_to_msg_id : Flag(0)) | (action.topicRootId ? Flag::f_top_msg_id : Flag(0)) | (action.options.sendAs ? Flag::f_send_as : Flag(0)) | (action.options.silent ? Flag::f_silent : Flag(0))), action.history->peer->input, _bot->inputUser, MTP_long(queryId), MTP_int(action.replyTo.bare), MTP_int(action.topicRootId.bare), (action.options.sendAs ? action.options.sendAs->input : MTP_inputPeerEmpty()) )).done([=] { _prolongId = 0; }).send(); }, _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) { const auto active = Core::App().activeWindow(); if (!active) { return; } _confirmAddBox = active->show(Box([=](not_null box) { const auto allowed = std::make_shared(); const auto done = [=](Fn close) { const auto state = ((*allowed) && (*allowed)->checked()) ? ToggledState::AllowedToWrite : ToggledState::Added; toggleInMenu(bot.user, state, [=] { if (callback) { callback(); } showToast(tr::lng_bot_add_to_menu_done(tr::now)); }); close(); }; Ui::ConfirmBox(box, { tr::lng_bot_add_to_menu( tr::now, lt_bot, Ui::Text::Bold(bot.name), Ui::Text::WithEntities), done, }); if (bot.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::toggleInMenu( not_null bot, ToggledState state, Fn callback) { using Flag = MTPmessages_ToggleBotInAttachMenu::Flag; _session->api().request(MTPmessages_ToggleBotInAttachMenu( MTP_flags((state == ToggledState::AllowedToWrite) ? Flag::f_write_allowed : Flag()), bot->inputUser, MTP_bool(state != ToggledState::Removed) )).done([=] { _requestId = 0; requestBots(); if (callback) { callback(); } }).fail([=] { cancel(); }).send(); } std::unique_ptr MakeAttachBotsMenu( not_null parent, not_null controller, not_null peer, Fn actionFactory, Fn attach) { if (!Data::CanSend(peer, ChatRestriction::SendInline)) { return nullptr; } auto result = std::make_unique( parent, st::dropdownMenuWithIcons); const auto bots = &peer->session().attachWebView(); const auto raw = result.get(); auto minimal = 0; if (Data::CanSend(peer, ChatRestriction::SendPhotos, false)) { ++minimal; raw->addAction(tr::lng_attach_photo_or_video(tr::now), [=] { attach(true); }, &st::menuIconPhoto); } const auto fileTypes = ChatRestriction::SendVideos | ChatRestriction::SendGifs | ChatRestriction::SendStickers | ChatRestriction::SendMusic | ChatRestriction::SendFiles; if (Data::CanSendAnyOf(peer, fileTypes)) { ++minimal; raw->addAction(tr::lng_attach_document(tr::now), [=] { attach(false); }, &st::menuIconFile); } for (const auto &bot : bots->attachBots()) { if (!PeerMatchesTypes(peer, bot.user, bot.types)) { continue; } const auto callback = [=] { bots->request( controller, actionFactory(), bot.user, { .fromMenu = true }); }; auto action = base::make_unique_q( raw, raw->menu()->st(), bot, callback); action->forceShown( ) | rpl::start_with_next([=](bool shown) { if (shown) { raw->setAutoHiding(false); } else { raw->hideAnimated(); raw->setAutoHiding(true); } }, action->lifetime()); raw->addAction(std::move(action)); } if (raw->actions().size() <= minimal) { return nullptr; } return result; } } // namespace InlineBots