// // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2026 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // #include "td/telegram/WebAppManager.h" #include "td/telegram/AccessRights.h" #include "td/telegram/AttachMenuManager.h" #include "td/telegram/AuthManager.h" #include "td/telegram/DialogManager.h" #include "td/telegram/FileReferenceManager.h" #include "td/telegram/files/FileManager.h" #include "td/telegram/Global.h" #include "td/telegram/MessagesManager.h" #include "td/telegram/SavedMessagesTopicId.h" #include "td/telegram/StateManager.h" #include "td/telegram/Td.h" #include "td/telegram/telegram_api.h" #include "td/telegram/TopDialogCategory.h" #include "td/telegram/UserManager.h" #include "td/telegram/WebApp.h" #include "td/telegram/WebAppOpenParameters.h" #include "td/utils/buffer.h" #include "td/utils/logging.h" #include "td/utils/misc.h" namespace td { class GetPopularAppBotsQuery final : public Td::ResultHandler { Promise> promise_; public: explicit GetPopularAppBotsQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(const string &offset, int32 limit) { send_query(G()->net_query_creator().create(telegram_api::bots_getPopularAppBots(offset, limit))); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } auto ptr = result_ptr.move_as_ok(); LOG(INFO) << "Receive result for GetPopularAppBotsQuery: " << to_string(ptr); vector user_ids; for (auto &user : ptr->users_) { auto user_id = td_->user_manager_->on_get_user(std::move(user), "GetPopularAppBotsQuery"); if (td_->user_manager_->is_user_bot(user_id)) { user_ids.push_back(td_->user_manager_->get_user_id_object(user_id, "GetPopularAppBotsQuery")); } } promise_.set_value(td_api::make_object(std::move(user_ids), ptr->next_offset_)); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; class GetBotAppQuery final : public Td::ResultHandler { Promise> promise_; public: explicit GetBotAppQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(telegram_api::object_ptr &&input_user, const string &short_name) { auto input_bot_app = telegram_api::make_object(std::move(input_user), short_name); send_query(G()->net_query_creator().create(telegram_api::messages_getBotApp(std::move(input_bot_app), 0))); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } auto ptr = result_ptr.move_as_ok(); LOG(INFO) << "Receive result for GetBotAppQuery: " << to_string(ptr); promise_.set_value(std::move(ptr)); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; class RequestAppWebViewQuery final : public Td::ResultHandler { Promise> promise_; public: explicit RequestAppWebViewQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(DialogId dialog_id, telegram_api::object_ptr &&input_user, const string &web_app_short_name, const string &start_parameter, const WebAppOpenParameters ¶meters, bool allow_write_access) { int32 flags = 0; auto theme_parameters = parameters.get_input_theme_parameters(); if (theme_parameters != nullptr) { flags |= telegram_api::messages_requestAppWebView::THEME_PARAMS_MASK; } if (!start_parameter.empty()) { flags |= telegram_api::messages_requestAppWebView::START_PARAM_MASK; } auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read); CHECK(input_peer != nullptr); auto input_bot_app = telegram_api::make_object(std::move(input_user), web_app_short_name); send_query(G()->net_query_creator().create(telegram_api::messages_requestAppWebView( flags, allow_write_access, parameters.is_compact(), parameters.is_full_screen(), std::move(input_peer), std::move(input_bot_app), start_parameter, std::move(theme_parameters), parameters.get_application_name()))); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } auto ptr = result_ptr.move_as_ok(); LOG(INFO) << "Receive result for RequestAppWebViewQuery: " << to_string(ptr); LOG_IF(ERROR, ptr->query_id_ != 0) << "Receive " << to_string(ptr); promise_.set_value(td_api::make_object(ptr->url_, ptr->same_origin_)); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; class RequestMainWebViewQuery final : public Td::ResultHandler { Promise> promise_; bool is_full_screen_ = false; public: explicit RequestMainWebViewQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(DialogId dialog_id, telegram_api::object_ptr &&input_user, const string &start_parameter, const WebAppOpenParameters ¶meters) { is_full_screen_ = parameters.is_full_screen(); int32 flags = 0; auto theme_parameters = parameters.get_input_theme_parameters(); if (theme_parameters != nullptr) { flags |= telegram_api::messages_requestMainWebView::THEME_PARAMS_MASK; } if (!start_parameter.empty()) { flags |= telegram_api::messages_requestMainWebView::START_PARAM_MASK; } auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Read); CHECK(input_peer != nullptr); send_query(G()->net_query_creator().create(telegram_api::messages_requestMainWebView( flags, parameters.is_compact(), is_full_screen_, std::move(input_peer), std::move(input_user), start_parameter, std::move(theme_parameters), parameters.get_application_name()))); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } auto ptr = result_ptr.move_as_ok(); LOG(INFO) << "Receive result for RequestMainWebViewQuery: " << to_string(ptr); LOG_IF(ERROR, ptr->query_id_ != 0) << "Receive " << to_string(ptr); td_api::object_ptr mode; if (is_full_screen_) { mode = td_api::make_object(); } else if (ptr->fullsize_) { mode = td_api::make_object(); } else { mode = td_api::make_object(); } promise_.set_value(td_api::make_object( td_api::make_object(ptr->url_, ptr->same_origin_), std::move(mode))); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; class RequestWebViewQuery final : public Td::ResultHandler { Promise> promise_; DialogId dialog_id_; UserId bot_user_id_; MessageTopic message_topic_; MessageInputReplyTo input_reply_to_; DialogId as_dialog_id_; bool from_attach_menu_ = false; public: explicit RequestWebViewQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(DialogId dialog_id, UserId bot_user_id, tl_object_ptr &&input_user, string &&url, const WebAppOpenParameters ¶meters, MessageTopic message_topic, MessageInputReplyTo input_reply_to, bool silent, DialogId as_dialog_id) { dialog_id_ = dialog_id; bot_user_id_ = bot_user_id; message_topic_ = std::move(message_topic); input_reply_to_ = std::move(input_reply_to); as_dialog_id_ = as_dialog_id; int32 flags = 0; auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write); CHECK(input_peer != nullptr); string start_parameter; bool from_bot_menu = false; if (begins_with(url, "start://")) { start_parameter = url.substr(8); url = string(); flags |= telegram_api::messages_requestWebView::START_PARAM_MASK; } else if (begins_with(url, "menu://")) { url = url.substr(7); from_bot_menu = true; flags |= telegram_api::messages_requestWebView::URL_MASK; } else if (!url.empty()) { flags |= telegram_api::messages_requestWebView::URL_MASK; } else { from_attach_menu_ = true; } auto theme_parameters = parameters.get_input_theme_parameters(); if (theme_parameters != nullptr) { flags |= telegram_api::messages_requestWebView::THEME_PARAMS_MASK; } auto reply_to = input_reply_to_.get_input_reply_to(td_, message_topic_); if (reply_to != nullptr) { flags |= telegram_api::messages_requestWebView::REPLY_TO_MASK; } telegram_api::object_ptr as_input_peer; if (as_dialog_id.is_valid()) { as_input_peer = td_->dialog_manager_->get_input_peer(as_dialog_id, AccessRights::Write); if (as_input_peer != nullptr) { flags |= telegram_api::messages_requestWebView::SEND_AS_MASK; } } send_query(G()->net_query_creator().create(telegram_api::messages_requestWebView( flags, from_bot_menu, silent, parameters.is_compact(), parameters.is_full_screen(), std::move(input_peer), std::move(input_user), url, start_parameter, std::move(theme_parameters), parameters.get_application_name(), std::move(reply_to), std::move(as_input_peer)))); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } auto ptr = result_ptr.move_as_ok(); LOG_IF(ERROR, ptr->query_id_ == 0) << "Receive " << to_string(ptr); td_->web_app_manager_->open_web_view(ptr->query_id_, dialog_id_, bot_user_id_, std::move(message_topic_), std::move(input_reply_to_), as_dialog_id_); promise_.set_value(td_api::make_object( ptr->query_id_, td_api::make_object(ptr->url_, ptr->same_origin_))); } void on_error(Status status) final { if (!td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "RequestWebViewQuery")) { if (from_attach_menu_) { td_->attach_menu_manager_->reload_attach_menu_bots(Promise()); } } promise_.set_error(std::move(status)); } }; class ProlongWebViewQuery final : public Td::ResultHandler { DialogId dialog_id_; public: void send(DialogId dialog_id, UserId bot_user_id, int64 query_id, const MessageTopic &message_topic, const MessageInputReplyTo &input_reply_to, bool silent, DialogId as_dialog_id) { dialog_id_ = dialog_id; auto input_peer = td_->dialog_manager_->get_input_peer(dialog_id, AccessRights::Write); auto r_input_user = td_->user_manager_->get_input_user(bot_user_id); if (input_peer == nullptr || r_input_user.is_error()) { return; } int32 flags = 0; auto reply_to = input_reply_to.get_input_reply_to(td_, message_topic); if (reply_to != nullptr) { flags |= telegram_api::messages_prolongWebView::REPLY_TO_MASK; } telegram_api::object_ptr as_input_peer; if (as_dialog_id.is_valid()) { as_input_peer = td_->dialog_manager_->get_input_peer(as_dialog_id, AccessRights::Write); if (as_input_peer != nullptr) { flags |= telegram_api::messages_prolongWebView::SEND_AS_MASK; } } send_query(G()->net_query_creator().create( telegram_api::messages_prolongWebView(flags, silent, std::move(input_peer), r_input_user.move_as_ok(), query_id, std::move(reply_to), std::move(as_input_peer)))); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } bool ptr = result_ptr.ok(); if (!ptr) { LOG(ERROR) << "Failed to prolong a web view"; } } void on_error(Status status) final { td_->dialog_manager_->on_get_dialog_error(dialog_id_, status, "ProlongWebViewQuery"); } }; class InvokeWebViewCustomMethodQuery final : public Td::ResultHandler { Promise> promise_; public: explicit InvokeWebViewCustomMethodQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(UserId bot_user_id, const string &method, const string ¶meters) { auto r_input_user = td_->user_manager_->get_input_user(bot_user_id); if (r_input_user.is_error()) { return on_error(r_input_user.move_as_error()); } send_query(G()->net_query_creator().create(telegram_api::bots_invokeWebViewCustomMethod( r_input_user.move_as_ok(), method, make_tl_object(parameters)))); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } auto result = result_ptr.move_as_ok(); promise_.set_value(td_api::make_object(result->data_)); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; class CheckDownloadFileParamsQuery final : public Td::ResultHandler { Promise promise_; public: explicit CheckDownloadFileParamsQuery(Promise &&promise) : promise_(std::move(promise)) { } void send(telegram_api::object_ptr &&input_user, const string &file_name, const string &url) { send_query(G()->net_query_creator().create( telegram_api::bots_checkDownloadFileParams(std::move(input_user), file_name, url))); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } if (!result_ptr.ok()) { return on_error(Status::Error(400, "The file can't be downloaded")); } promise_.set_value(Unit()); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; WebAppManager::WebAppManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) { } void WebAppManager::start_up() { class StateCallback final : public StateManager::Callback { public: explicit StateCallback(ActorId parent) : parent_(std::move(parent)) { } bool on_online(bool is_online) final { if (is_online) { send_closure(parent_, &WebAppManager::on_online, is_online); } return parent_.is_alive(); } private: ActorId parent_; }; send_closure(G()->state_manager(), &StateManager::add_callback, make_unique(actor_id(this))); } void WebAppManager::tear_down() { parent_.reset(); } void WebAppManager::on_online(bool is_online) { if (is_online) { ping_web_view(); } else { ping_web_view_timeout_.cancel_timeout(); } } void WebAppManager::ping_web_view_static(void *td_void) { if (G()->close_flag()) { return; } CHECK(td_void != nullptr); auto td = static_cast(td_void); td->web_app_manager_->ping_web_view(); } void WebAppManager::ping_web_view() { if (G()->close_flag() || opened_web_views_.empty()) { return; } for (const auto &it : opened_web_views_) { const auto &opened_web_view = it.second; bool silent = td_->messages_manager_->get_dialog_silent_send_message(opened_web_view.dialog_id_); td_->create_handler()->send(opened_web_view.dialog_id_, opened_web_view.bot_user_id_, it.first, opened_web_view.message_topic_, opened_web_view.input_reply_to_, silent, opened_web_view.as_dialog_id_); } schedule_ping_web_view(); } void WebAppManager::schedule_ping_web_view() { ping_web_view_timeout_.set_callback(ping_web_view_static); ping_web_view_timeout_.set_callback_data(static_cast(td_)); ping_web_view_timeout_.set_timeout_in(PING_WEB_VIEW_TIMEOUT); } void WebAppManager::get_popular_app_bots(const string &offset, int32 limit, Promise> &&promise) { if (limit <= 0) { return promise.set_error(400, "Limit must be positive"); } td_->create_handler(std::move(promise))->send(offset, limit); } void WebAppManager::get_web_app(UserId bot_user_id, const string &web_app_short_name, Promise> &&promise) { TRY_RESULT_PROMISE(promise, input_user, td_->user_manager_->get_input_user(bot_user_id)); TRY_RESULT_PROMISE(promise, bot_data, td_->user_manager_->get_bot_data(bot_user_id)); auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), bot_user_id, web_app_short_name, promise = std::move(promise)]( Result> result) mutable { send_closure(actor_id, &WebAppManager::on_get_web_app, bot_user_id, std::move(web_app_short_name), std::move(result), std::move(promise)); }); td_->create_handler(std::move(query_promise))->send(std::move(input_user), web_app_short_name); } void WebAppManager::on_get_web_app(UserId bot_user_id, string web_app_short_name, Result> result, Promise> promise) { G()->ignore_result_if_closing(result); if (result.is_error() && result.error().message() == "BOT_APP_INVALID") { return promise.set_value(nullptr); } TRY_RESULT_PROMISE(promise, bot_app, std::move(result)); if (bot_app->app_->get_id() != telegram_api::botApp::ID) { CHECK(bot_app->app_->get_id() != telegram_api::botAppNotModified::ID); LOG(ERROR) << "Receive " << to_string(bot_app); return promise.set_error(500, "Receive invalid response"); } WebApp web_app(td_, telegram_api::move_object_as(bot_app->app_), DialogId(bot_user_id)); auto file_ids = web_app.get_file_ids(td_); if (!file_ids.empty()) { auto file_source_id = get_web_app_file_source_id(bot_user_id, web_app_short_name); for (auto file_id : file_ids) { td_->file_manager_->add_file_source(file_id, file_source_id, "on_get_web_app"); } } promise.set_value(td_api::make_object(web_app.get_web_app_object(td_), bot_app->request_write_access_, !bot_app->inactive_)); } void WebAppManager::reload_web_app(UserId bot_user_id, const string &web_app_short_name, Promise &&promise) { get_web_app(bot_user_id, web_app_short_name, PromiseCreator::lambda( [promise = std::move(promise)](Result> result) mutable { if (result.is_error()) { promise.set_error(result.move_as_error()); } else { promise.set_value(Unit()); } })); } void WebAppManager::request_app_web_view(DialogId dialog_id, UserId bot_user_id, string &&web_app_short_name, string &&start_parameter, const WebAppOpenParameters ¶meters, bool allow_write_access, Promise> &&promise) { if (!td_->dialog_manager_->have_input_peer(dialog_id, false, AccessRights::Read) || td_->dialog_manager_->is_monoforum_channel(dialog_id)) { dialog_id = DialogId(bot_user_id); } TRY_RESULT_PROMISE(promise, input_user, td_->user_manager_->get_input_user(bot_user_id)); TRY_RESULT_PROMISE(promise, bot_data, td_->user_manager_->get_bot_data(bot_user_id)); on_dialog_used(TopDialogCategory::BotApp, DialogId(bot_user_id), G()->unix_time()); td_->create_handler(std::move(promise)) ->send(dialog_id, std::move(input_user), web_app_short_name, start_parameter, parameters, allow_write_access); } void WebAppManager::request_main_web_view(DialogId dialog_id, UserId bot_user_id, string &&start_parameter, const WebAppOpenParameters ¶meters, Promise> &&promise) { if (!td_->dialog_manager_->have_input_peer(dialog_id, false, AccessRights::Read) || td_->dialog_manager_->is_monoforum_channel(dialog_id)) { dialog_id = DialogId(bot_user_id); } TRY_RESULT_PROMISE(promise, input_user, td_->user_manager_->get_input_user(bot_user_id)); TRY_RESULT_PROMISE(promise, bot_data, td_->user_manager_->get_bot_data(bot_user_id)); if (!bot_data.has_main_app) { return promise.set_error(400, "The bot has no main Mini App"); } on_dialog_used(TopDialogCategory::BotApp, DialogId(bot_user_id), G()->unix_time()); td_->create_handler(std::move(promise)) ->send(dialog_id, std::move(input_user), start_parameter, parameters); } void WebAppManager::request_web_view(DialogId dialog_id, UserId bot_user_id, const td_api::object_ptr &topic_id, td_api::object_ptr &&reply_to, string &&url, const WebAppOpenParameters ¶meters, Promise> &&promise) { TRY_STATUS_PROMISE(promise, td_->user_manager_->get_bot_data(bot_user_id)); TRY_RESULT_PROMISE(promise, input_user, td_->user_manager_->get_input_user(bot_user_id)); TRY_RESULT_PROMISE(promise, bot_data, td_->user_manager_->get_bot_data(bot_user_id)); TRY_STATUS_PROMISE( promise, td_->dialog_manager_->check_dialog_access(dialog_id, false, AccessRights::Write, "request_web_view")); TRY_RESULT_PROMISE(promise, message_topic, MessageTopic::get_send_message_topic(td_, dialog_id, topic_id)); on_dialog_used(TopDialogCategory::BotApp, DialogId(bot_user_id), G()->unix_time()); auto input_reply_to = td_->messages_manager_->create_message_input_reply_to(dialog_id, message_topic, std::move(reply_to), false); bool silent = td_->messages_manager_->get_dialog_silent_send_message(dialog_id); DialogId as_dialog_id = td_->messages_manager_->get_dialog_default_send_message_as_dialog_id(dialog_id); td_->create_handler(std::move(promise)) ->send(dialog_id, bot_user_id, std::move(input_user), std::move(url), parameters, std::move(message_topic), std::move(input_reply_to), silent, as_dialog_id); } void WebAppManager::open_web_view(int64 query_id, DialogId dialog_id, UserId bot_user_id, MessageTopic &&message_topic, MessageInputReplyTo &&input_reply_to, DialogId as_dialog_id) { if (query_id == 0) { LOG(ERROR) << "Receive Web App query identifier == 0"; return; } if (opened_web_views_.empty()) { schedule_ping_web_view(); } OpenedWebView opened_web_view; opened_web_view.dialog_id_ = dialog_id; opened_web_view.bot_user_id_ = bot_user_id; opened_web_view.message_topic_ = std::move(message_topic); opened_web_view.input_reply_to_ = std::move(input_reply_to); opened_web_view.as_dialog_id_ = as_dialog_id; opened_web_views_.emplace(query_id, std::move(opened_web_view)); } void WebAppManager::close_web_view(int64 query_id, Promise &&promise) { opened_web_views_.erase(query_id); if (opened_web_views_.empty()) { ping_web_view_timeout_.cancel_timeout(); } promise.set_value(Unit()); } void WebAppManager::invoke_web_view_custom_method(UserId bot_user_id, const string &method, const string ¶meters, Promise> &&promise) { td_->create_handler(std::move(promise))->send(bot_user_id, method, parameters); } void WebAppManager::check_download_file_params(UserId bot_user_id, const string &file_name, const string &url, Promise &&promise) { TRY_RESULT_PROMISE(promise, input_user, td_->user_manager_->get_input_user(bot_user_id)); if (file_name.size() >= 256u || url.size() > 32768u || file_name.find('/') != string::npos || file_name.find('\\') != string::npos) { return promise.set_error(400, "The file can't be downloaded"); } td_->create_handler(std::move(promise))->send(std::move(input_user), file_name, url); } FileSourceId WebAppManager::get_web_app_file_source_id(UserId user_id, const string &short_name) { if (G()->close_flag()) { return FileSourceId(); } if (!user_id.is_valid() || !td_->auth_manager_->is_authorized() || td_->auth_manager_->is_bot()) { return FileSourceId(); } auto &source_id = web_app_file_source_ids_[user_id][short_name]; if (!source_id.is_valid()) { source_id = td_->file_reference_manager_->create_web_app_file_source(user_id, short_name); } VLOG(file_references) << "Return " << source_id << " for Web App " << user_id << '/' << short_name; return source_id; } } // namespace td