diff --git a/td/generate/scheme/td_api.tl b/td/generate/scheme/td_api.tl index 1d2d112aa..49135b6d4 100644 --- a/td/generate/scheme/td_api.tl +++ b/td/generate/scheme/td_api.tl @@ -555,7 +555,7 @@ userTypeDeleted = UserType; //@is_inline True, if the bot supports inline queries //@inline_query_placeholder Placeholder for inline queries (displayed on the application input field) //@need_location True, if the location of the user is expected to be sent with every inline query to this bot -//@can_be_added_to_attachment_menu True, if the bot can be added to attachment menu +//@can_be_added_to_attachment_menu True, if the bot can be added to attachment or side menu userTypeBot can_be_edited:Bool can_join_groups:Bool can_read_all_group_messages:Bool is_inline:Bool inline_query_placeholder:string need_location:Bool can_be_added_to_attachment_menu:Bool = UserType; //@description No information on the user besides the user identifier is available, yet this user has not been deleted. This object is extremely rare and must be handled like a deleted user. It is not possible to perform any actions on users of this type @@ -5074,6 +5074,13 @@ internalLinkTypeRestorePurchases = InternalLinkType; //@description The link is a link to application settings internalLinkTypeSettings = InternalLinkType; +//@description The link is a link to a bot, which can be installed to the side menu. Call searchPublicChat with the given bot username, check that the user is a bot and can be added to attachment menu. +//-Then, use getAttachmentMenuBot to receive information about the bot. If the bot isn't added to side menu, then user needs to confirm adding the bot to side and attachment menu. +//-If user confirms adding, then use toggleBotIsAddedToAttachmentMenu to add the bot. If the bot is added to attachment menu, then use getWebAppUrl with the given URL +//@bot_username Username of the bot +//@url URL to be passed to getWebAppUrl +internalLinkTypeSideMenuBot bot_username:string url:string = InternalLinkType; + //@description The link is a link to a sticker set. Call searchStickerSet with the given sticker set name to process the link and show the sticker set //@sticker_set_name Name of the sticker set //@expect_custom_emoji True, if the sticker set is expected to contain custom emoji diff --git a/td/telegram/LinkManager.cpp b/td/telegram/LinkManager.cpp index a433a36e1..1d0139658 100644 --- a/td/telegram/LinkManager.cpp +++ b/td/telegram/LinkManager.cpp @@ -597,6 +597,22 @@ class LinkManager::InternalLinkSettings final : public InternalLink { } }; +class LinkManager::InternalLinkSideMenuBot final : public InternalLink { + string bot_username_; + string url_; + + td_api::object_ptr get_internal_link_type_object() const final { + return td_api::make_object(bot_username_, url_); + } + + public: + InternalLinkSideMenuBot(string bot_username, string start_parameter) : bot_username_(std::move(bot_username)) { + if (!start_parameter.empty()) { + url_ = PSTRING() << "start://" << start_parameter; + } + } +}; + class LinkManager::InternalLinkStickerSet final : public InternalLink { string sticker_set_name_; bool expect_custom_emoji_; @@ -1224,6 +1240,11 @@ unique_ptr LinkManager::parse_tg_link_query(Slice que return td::make_unique(std::move(username), StoryId(to_integer(arg.second))); } } + if (url_query.has_arg("startapp") && !url_query.has_arg("appname")) { + // resolve?domain=&startapp= + // resolve?domain=&startapp= + return td::make_unique(std::move(username), url_query.get_arg("startapp").str()); + } if (!url_query.get_arg("attach").empty()) { // resolve?domain=&attach= // resolve?domain=&attach=&startattach= @@ -1616,6 +1637,11 @@ unique_ptr LinkManager::parse_t_me_link_query(Slice q return td::make_unique(std::move(username), std::move(administrator_rights)); } } + if (arg.first == "startapp" && is_valid_start_parameter(arg.second)) { + // /?startapp + // /?startapp= + return td::make_unique(std::move(username), arg.second); + } if (arg.first == "game" && is_valid_game_name(arg.second)) { // /?game= return td::make_unique(std::move(username), arg.second); @@ -1727,7 +1753,11 @@ Result LinkManager::get_internal_link_impl(const td_api::InternalLinkTyp if (!begins_with(link->url_, "start://")) { return Status::Error(400, "Unsupported link URL specified"); } - start_parameter = PSTRING() << '=' << Slice(link->url_).substr(8); + auto start_parameter_slice = Slice(link->url_).substr(8); + if (start_parameter_slice.empty() || !is_valid_start_parameter(start_parameter_slice)) { + return Status::Error(400, "Invalid start parameter specified"); + } + start_parameter = PSTRING() << '=' << start_parameter_slice; } if (link->target_chat_ == nullptr) { return Status::Error(400, "Target chat must be non-empty"); @@ -1917,6 +1947,28 @@ Result LinkManager::get_internal_link_impl(const td_api::InternalLinkTyp return Status::Error("HTTP link is unavailable for the link type"); } return "tg://settings/auto_delete"; + case td_api::internalLinkTypeSideMenuBot::ID: { + auto link = static_cast(type_ptr); + if (!is_valid_username(link->bot_username_)) { + return Status::Error(400, "Invalid bot username specified"); + } + string start_parameter; + if (!link->url_.empty()) { + if (!begins_with(link->url_, "start://")) { + return Status::Error(400, "Unsupported link URL specified"); + } + auto start_parameter_slice = Slice(link->url_).substr(8); + if (start_parameter_slice.empty() || !is_valid_start_parameter(start_parameter_slice)) { + return Status::Error(400, "Invalid start parameter specified"); + } + start_parameter = PSTRING() << '=' << start_parameter_slice; + } + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << link->bot_username_ << "&startapp" << start_parameter; + } else { + return PSTRING() << get_t_me_url() << link->bot_username_ << "?startapp" << start_parameter; + } + } case td_api::internalLinkTypeEditProfileSettings::ID: if (!is_internal) { return Status::Error("HTTP link is unavailable for the link type"); @@ -2197,7 +2249,7 @@ Result LinkManager::get_internal_link_impl(const td_api::InternalLinkTyp } string start_parameter; if (!link->start_parameter_.empty()) { - start_parameter = PSTRING() << (is_internal ? '&' : '?') << "startapp=" << url_encode(link->start_parameter_); + start_parameter = PSTRING() << (is_internal ? '&' : '?') << "startapp=" << link->start_parameter_; } if (is_internal) { return PSTRING() << "tg://resolve?domain=" << link->bot_username_ << "&appname=" << link->web_app_short_name_ diff --git a/td/telegram/LinkManager.h b/td/telegram/LinkManager.h index 5a9e4e0a0..8c4d19999 100644 --- a/td/telegram/LinkManager.h +++ b/td/telegram/LinkManager.h @@ -143,6 +143,7 @@ class LinkManager final : public Actor { class InternalLinkQrCodeAuthentication; class InternalLinkRestorePurchases; class InternalLinkSettings; + class InternalLinkSideMenuBot; class InternalLinkStickerSet; class InternalLinkStory; class InternalLinkTheme; diff --git a/test/link.cpp b/test/link.cpp index 5c851cff4..14d42a2de 100644 --- a/test/link.cpp +++ b/test/link.cpp @@ -313,6 +313,11 @@ static auto settings() { return td::td_api::make_object(); } +static auto side_menu_bot(const td::string &bot_username, const td::string &start_parameter) { + return td::td_api::make_object( + bot_username, start_parameter.empty() ? td::string() : "start://" + start_parameter); +} + static auto sticker_set(const td::string &sticker_set_name, bool expect_custom_emoji) { return td::td_api::make_object(sticker_set_name, expect_custom_emoji); } @@ -1104,6 +1109,23 @@ TEST(Link, parse_internal_link_part4) { parse_internal_link("t.me/username/", public_chat("username")); parse_internal_link("https://telegram.dog/tele%63ram/t%63st", web_app("telecram", "tcst", "")); + parse_internal_link("tg:resolve?domain=username&startapp=aasdasd", side_menu_bot("username", "aasdasd")); + parse_internal_link("TG://resolve?domain=username&startapp=&startapp=123asd", side_menu_bot("username", "")); + parse_internal_link("TG://test@resolve?domain=username&startapp=asd", nullptr); + parse_internal_link("tg:resolve:80?domain=username&startapp=asd", nullptr); + parse_internal_link("tg:http://resolve?domain=username&startapp=asd", nullptr); + parse_internal_link("tg:https://resolve?domain=username&startapp=asd", nullptr); + parse_internal_link("tg:resolve?domain=&startapp=asd", unknown_deep_link("tg://resolve?domain=&startapp=asd")); + parse_internal_link("tg:resolve?domain=telegram&&&&&&&startapp=%41", side_menu_bot("telegram", "A")); + parse_internal_link("tg:resolve?domain=telegram&&&&&&&startapp=%41b", side_menu_bot("telegram", "Ab")); + parse_internal_link("tg:resolve?domain=telegram&&&&&&&startapp=%41bc", side_menu_bot("telegram", "Abc")); + + parse_internal_link("t.me/username?startapp=qwe", side_menu_bot("username", "qwe")); + parse_internal_link("t.me/username?12312&startapp=qwe", side_menu_bot("username", "qwe")); + parse_internal_link("t.me/username?startapp=0", side_menu_bot("username", "0")); + parse_internal_link("https://telegram.dog/tele%63ram?startapp=t%63st", side_menu_bot("telecram", "tcst")); + parse_internal_link("https://telegram.dog?startapp=t%63st", nullptr); + parse_internal_link("tg:resolve?domain=username&Game=asd", public_chat("username")); parse_internal_link("TG://test@resolve?domain=username", nullptr); parse_internal_link("tg:resolve:80?domain=username", nullptr);