Implement new bot web-app methods.

This commit is contained in:
John Preston 2023-08-29 21:36:12 +04:00
parent 8255de1ba8
commit d77c7a70ab
14 changed files with 478 additions and 236 deletions

View File

@ -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";

View File

@ -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<UserData*> 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(

View File

@ -290,12 +290,14 @@ public:
Data::ResolvedForwardDraft &&draft,
const SendAction &action,
FnMut<void()> &&successCallback = nullptr);
void shareContact(
FullMsgId shareContact(
const QString &phone,
const QString &firstName,
const QString &lastName,
const SendAction &action);
void shareContact(not_null<UserData*> user, const SendAction &action);
FullMsgId shareContact(
not_null<UserData*> user,
const SendAction &action);
void applyAffectedMessages(
not_null<PeerData*> 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,

View File

@ -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());

View File

@ -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;
};

View File

@ -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.

View File

@ -439,6 +439,7 @@ struct ActionBotAllowed {
Utf8String app;
Utf8String domain;
bool attachMenu = false;
bool fromRequest = false;
};
struct ActionSecureValuesSent {

View File

@ -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))

View File

@ -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);

View File

@ -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();

View File

@ -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<uint64>();
_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<QString> 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<Data::Thread*> thread) {
controller->switchInlineQuery(thread, bot, query);
};
ShowChooseBox(
controller,
types,
done,
tr::lng_inline_switch_choose());
}
crl::on_main(this, [=] { cancel(); });
}
void AttachWebView::botCheckWriteAccess(Fn<void(bool allowed)> callback) {
_session->api().request(MTPbots_CanSendMessage(
_bot->inputUser
)).done([=](const MTPBool &result) {
callback(mtpIsTrue(result));
}).fail([=] {
callback(false);
}).send();
}
void AttachWebView::botAllowWriteAccess(Fn<void(bool allowed)> 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<void(bool shared)> 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<rpl::lifetime>();
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<const HistoryItem*> 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<Window::SessionController*> 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<Api::SendAction> AttachWebView::lookupLastAction(
}
void AttachWebView::resolve() {
Expects(!_panel);
resolveUsername(_botUsername, [=](not_null<PeerData*> 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<uint64>();
_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<QString> 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<Data::Thread*> 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<Ui::BotWebView::Panel>>(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<Ui::BotWebView::Delegate*>(this),
.menuButtons = buttons,
.handleMenuButton = handleMenuButton,
.themeParams = [] { return Window::Theme::WebViewParams(); },
.allowClipboardRead = allowClipboardRead,
});
*panel = _panel.get();
started(queryId);
}

View File

@ -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<Main::Session*> 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<QString> chatTypes,
QString query) override;
void botCheckWriteAccess(Fn<void(bool allowed)> callback) override;
void botAllowWriteAccess(Fn<void(bool allowed)> callback) override;
void botSharePhone(Fn<void(bool shared)> callback) override;
void botInvokeCustomMethod(
Ui::BotWebView::CustomMethodRequest request) override;
void botClose() override;
[[nodiscard]] static Context LookupContext(
not_null<Window::SessionController*> controller,
const Api::SendAction &action);
@ -185,6 +203,8 @@ private:
std::unique_ptr<Context> _context;
std::unique_ptr<Context> _lastShownContext;
QString _lastShownUrl;
uint64 _lastShownQueryId = 0;
QString _lastShownButtonText;
UserData *_bot = nullptr;
QString _botUsername;
QString _botAppName;

View File

@ -315,25 +315,12 @@ Panel::Progress::Progress(QWidget *parent, Fn<QRect()> rect)
Panel::Panel(
const QString &userDataPath,
rpl::producer<QString> title,
Fn<bool(QString)> handleLocalUri,
Fn<void(QString)> handleInvoice,
Fn<void(QByteArray)> sendData,
Fn<void(std::vector<QString>, QString)> switchInlineQuery,
Fn<void()> close,
QString phone,
not_null<Delegate*> delegate,
MenuButtons menuButtons,
Fn<void(MenuButton)> handleMenuButton,
Fn<Webview::ThemeParams()> 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<SeparatePanel>())
, _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<QString> 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<QString>{
@ -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<Panel> Show(Args &&args) {
const auto params = args.themeParams();
auto result = std::make_unique<Panel>(
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) {

View File

@ -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<MenuButton>;
using CustomMethodResult = base::expected<QByteArray, QString>;
struct CustomMethodRequest {
QString method;
QByteArray params;
Fn<void(CustomMethodResult)> 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<QString> chatTypes,
QString query) = 0;
virtual void botCheckWriteAccess(Fn<void(bool allowed)> callback) = 0;
virtual void botAllowWriteAccess(Fn<void(bool allowed)> callback) = 0;
virtual void botSharePhone(Fn<void(bool shared)> 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<QString> title,
Fn<bool(QString)> handleLocalUri,
Fn<void(QString)> handleInvoice,
Fn<void(QByteArray)> sendData,
Fn<void(std::vector<QString>, QString)> switchInlineQuery,
Fn<void()> close,
QString phone,
not_null<Delegate*> delegate,
MenuButtons menuButtons,
Fn<void(MenuButton)> handleMenuButton,
Fn<Webview::ThemeParams()> 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<bool(QString)> _handleLocalUri;
Fn<void(QString)> _handleInvoice;
Fn<void(QByteArray)> _sendData;
Fn<void(std::vector<QString>, QString)> _switchInlineQuery;
Fn<void()> _close;
QString _phone;
const not_null<Delegate*> _delegate;
bool _closeNeedConfirmation = false;
MenuButtons _menuButtons = {};
Fn<void(MenuButton)> _handleMenuButton;
std::unique_ptr<SeparatePanel> _widget;
std::unique_ptr<WebviewWithLifetime> _webview;
std::unique_ptr<RpWidget> _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<QString> title;
rpl::producer<QString> bottom;
Fn<bool(QString)> handleLocalUri;
Fn<void(QString)> handleInvoice;
Fn<void(QByteArray)> sendData;
Fn<void(std::vector<QString>, QString)> switchInlineQuery;
Fn<void()> close;
QString phone;
not_null<Delegate*> delegate;
MenuButtons menuButtons;
Fn<void(MenuButton)> handleMenuButton;
Fn<Webview::ThemeParams()> themeParams;
bool allowClipboardRead = false;
};
[[nodiscard]] std::unique_ptr<Panel> Show(Args &&args);