From 672ef60828bc23e1c5723cd105c16eb84d611a8f Mon Sep 17 00:00:00 2001 From: levlam Date: Thu, 2 Mar 2023 16:21:36 +0300 Subject: [PATCH] Add td_api::getInternalLink. --- td/generate/scheme/td_api.tl | 19 +- td/telegram/ContactsManager.cpp | 4 +- td/telegram/LinkManager.cpp | 566 +++++++++++++++++++++++++++++++- td/telegram/LinkManager.h | 6 +- td/telegram/Td.cpp | 10 + td/telegram/Td.h | 2 + td/telegram/net/Proxy.cpp | 14 +- td/telegram/net/Proxy.h | 2 +- test/link.cpp | 44 ++- 9 files changed, 634 insertions(+), 33 deletions(-) diff --git a/td/generate/scheme/td_api.tl b/td/generate/scheme/td_api.tl index b717dde26..60ad8b7d3 100644 --- a/td/generate/scheme/td_api.tl +++ b/td/generate/scheme/td_api.tl @@ -4589,6 +4589,13 @@ internalLinkTypeAuthenticationCode code:string = InternalLinkType; //@description The link is a link to a background. Call searchBackground with the given background name to process the link @background_name Name of the background internalLinkTypeBackground background_name:string = InternalLinkType; +//@description The link is a link to a Telegram bot, which is supposed to be added to a channel chat as an administrator. Call searchPublicChat with the given bot username and check that the user is a bot, +//-ask the current user to select a channel chat to add the bot to as an administrator. Then, call getChatMember to receive the current bot rights in the chat and if the bot already is an administrator, +//-check that the current user can edit its administrator rights and combine received rights with the requested administrator rights. Then, show confirmation box to the user, and call setChatMemberStatus with the chosen chat and confirmed rights +//@bot_username Username of the bot +//@administrator_rights Expected administrator rights for the bot +internalLinkTypeBotAddToChannel bot_username:string administrator_rights:chatAdministratorRights = InternalLinkType; + //@description The link is a link to a chat with a Telegram bot. Call searchPublicChat with the given bot username, check that the user is a bot, show START button in the chat with the bot, //-and then call sendBotStartMessage with the given start parameter after the button is pressed //@bot_username Username of the bot @@ -4607,13 +4614,6 @@ internalLinkTypeBotStart bot_username:string start_parameter:string autostart:Bo //@administrator_rights Expected administrator rights for the bot; may be null internalLinkTypeBotStartInGroup bot_username:string start_parameter:string administrator_rights:chatAdministratorRights = InternalLinkType; -//@description The link is a link to a Telegram bot, which is supposed to be added to a channel chat as an administrator. Call searchPublicChat with the given bot username and check that the user is a bot, -//-ask the current user to select a channel chat to add the bot to as an administrator. Then, call getChatMember to receive the current bot rights in the chat and if the bot already is an administrator, -//-check that the current user can edit its administrator rights and combine received rights with the requested administrator rights. Then, show confirmation box to the user, and call setChatMemberStatus with the chosen chat and confirmed rights -//@bot_username Username of the bot -//@administrator_rights Expected administrator rights for the bot -internalLinkTypeBotAddToChannel bot_username:string administrator_rights:chatAdministratorRights = InternalLinkType; - //@description The link is a link to the change phone number section of the app internalLinkTypeChangePhoneNumber = InternalLinkType; @@ -4668,7 +4668,7 @@ internalLinkTypePassportDataRequest bot_user_id:int53 scope:string public_key:st //@phone_number Phone number value from the link internalLinkTypePhoneNumberConfirmation hash:string phone_number:string = InternalLinkType; -//@description The link is a link to the Premium features screen of the applcation from which the user can subscribe to Telegram Premium. Call getPremiumFeatures with the given referrer to process the link @referrer Referrer specified in the link +//@description The link is a link to the Premium features screen of the application from which the user can subscribe to Telegram Premium. Call getPremiumFeatures with the given referrer to process the link @referrer Referrer specified in the link internalLinkTypePremiumFeatures referrer:string = InternalLinkType; //@description The link is a link to the privacy and security section of the app settings @@ -6673,6 +6673,9 @@ openMessageContent chat_id:int53 message_id:int53 = Ok; //@description Informs TDLib that a message with an animated emoji was clicked by the user. Returns a big animated sticker to be played or a 404 error if usual animation needs to be played @chat_id Chat identifier of the message @message_id Identifier of the clicked message clickAnimatedEmojiMessage chat_id:int53 message_id:int53 = Sticker; +//@description Returns an HTTPS or a tg: link with the given type. Can be called before authorization @type Expected type of the link @is_http Pass true to create an HTTPS link (only available for some link types); pass false to create a tg: link +getInternalLink type:InternalLinkType is_http:Bool = HttpUrl; + //@description Returns information about the type of an internal link. Returns a 404 error if the link is not internal. Can be called before authorization @link The link getInternalLinkType link:string = InternalLinkType; diff --git a/td/telegram/ContactsManager.cpp b/td/telegram/ContactsManager.cpp index ea4c3945e..8ec3ec67f 100644 --- a/td/telegram/ContactsManager.cpp +++ b/td/telegram/ContactsManager.cpp @@ -15828,8 +15828,8 @@ void ContactsManager::get_user_link_impl(Promiseclose_status()); const auto *u = get_user(get_my_id()); if (u != nullptr && u->usernames.has_first_username()) { - return promise.set_value( - td_api::make_object(LinkManager::get_public_chat_link(u->usernames.get_first_username()), 0)); + return promise.set_value(td_api::make_object( + LinkManager::get_public_dialog_link(u->usernames.get_first_username(), true), 0)); } export_contact_token(td_, std::move(promise)); } diff --git a/td/telegram/LinkManager.cpp b/td/telegram/LinkManager.cpp index 033c831ec..f1a2a2b93 100644 --- a/td/telegram/LinkManager.cpp +++ b/td/telegram/LinkManager.cpp @@ -133,6 +133,50 @@ static AdministratorRights get_administrator_rights(Slice rights, bool for_chann for_channel ? ChannelType::Broadcast : ChannelType::Megagroup); } +static string get_admin_string(AdministratorRights rights) { + vector admin_rights; + if (rights.can_change_info_and_settings()) { + admin_rights.emplace_back("change_info"); + } + if (rights.can_post_messages()) { + admin_rights.emplace_back("post_messages"); + } + if (rights.can_edit_messages()) { + admin_rights.emplace_back("edit_messages"); + } + if (rights.can_delete_messages()) { + admin_rights.emplace_back("delete_messages"); + } + if (rights.can_restrict_members()) { + admin_rights.emplace_back("restrict_members"); + } + if (rights.can_invite_users()) { + admin_rights.emplace_back("invite_users"); + } + if (rights.can_pin_messages()) { + admin_rights.emplace_back("pin_messages"); + } + if (rights.can_manage_topics()) { + admin_rights.emplace_back("manage_topics"); + } + if (rights.can_promote_members()) { + admin_rights.emplace_back("promote_members"); + } + if (rights.can_manage_calls()) { + admin_rights.emplace_back("manage_video_chats"); + } + if (rights.is_anonymous()) { + admin_rights.emplace_back("anonymous"); + } + if (rights.can_manage_dialog()) { + admin_rights.emplace_back("manage_chat"); + } + if (admin_rights.empty()) { + return string(); + } + return "&admin=" + implode(admin_rights, '+'); +} + td_api::object_ptr get_target_chat_chosen(Slice chat_types) { bool allow_users = false; bool allow_bots = false; @@ -1069,14 +1113,14 @@ unique_ptr LinkManager::parse_tg_link_query(Slice que }; if (path.size() == 1 && path[0] == "resolve") { - if (is_valid_username(get_arg("domain"))) { + auto username = get_arg("domain"); + if (is_valid_username(username)) { if (has_arg("post")) { // resolve?domain=&post=12345&single&thread=&comment=&t= return td::make_unique( PSTRING() << "tg://resolve" << copy_arg("domain") << copy_arg("post") << copy_arg("single") << copy_arg("thread") << copy_arg("comment") << copy_arg("t")); } - auto username = get_arg("domain"); for (auto &arg : url_query.args_) { if (arg.first == "voicechat" || arg.first == "videochat" || arg.first == "livestream") { // resolve?domain=&videochat @@ -1110,6 +1154,7 @@ unique_ptr LinkManager::parse_tg_link_query(Slice que return td::make_unique(std::move(username), arg.second); } if (arg.first == "appname" && is_valid_web_app_name(arg.second)) { + // resolve?domain=&appname= // resolve?domain=&appname=&startapp= return td::make_unique(std::move(username), arg.second, url_query.get_arg("startapp").str()); @@ -1128,7 +1173,7 @@ unique_ptr LinkManager::parse_tg_link_query(Slice que std::move(username), url_query.get_arg("startattach")); } if (username == "telegrampassport") { - // resolve?domain=telegrampassport&bot_id=&scope=&public_key=&nonce= + // resolve?domain=telegrampassport&bot_id=...&scope=...&public_key=...&nonce=...&callback_url=... return get_internal_link_passport(query, url_query.args_); } // resolve?domain= @@ -1162,7 +1207,7 @@ unique_ptr LinkManager::parse_tg_link_query(Slice que // restore_purchases return td::make_unique(); } else if (path.size() == 1 && path[0] == "passport") { - // passport?bot_id=&scope=&public_key=&nonce= + // passport?bot_id=...&scope=...&public_key=...&nonce=...&callback_url=... return get_internal_link_passport(query, url_query.args_); } else if (path.size() == 1 && path[0] == "premium_offer") { // premium_offer?ref= @@ -1206,7 +1251,7 @@ unique_ptr LinkManager::parse_tg_link_query(Slice que // join?invite= if (has_arg("invite")) { auto invite_hash = get_url_query_hash(true, url_query); - if (!invite_hash.empty() && !is_valid_phone_number(invite_hash)) { + if (!invite_hash.empty() && !is_valid_phone_number(invite_hash) && is_base64url_characters(invite_hash)) { return td::make_unique(PSTRING() << "tg:join?invite=" << url_encode(invite_hash)); } } @@ -1336,7 +1381,7 @@ unique_ptr LinkManager::parse_t_me_link_query(Slice q } else if (path[0] == "joinchat") { if (path.size() >= 2 && !path[1].empty()) { auto invite_hash = get_url_query_hash(false, url_query); - if (!invite_hash.empty() && !is_valid_phone_number(invite_hash)) { + if (!invite_hash.empty() && !is_valid_phone_number(invite_hash) && is_base64url_characters(invite_hash)) { // /joinchat/ return td::make_unique(PSTRING() << "tg:join?invite=" << url_encode(invite_hash)); } @@ -1354,7 +1399,7 @@ unique_ptr LinkManager::parse_t_me_link_query(Slice q } // /+ return std::move(user_link); - } else if (!invite_hash.empty()) { + } else if (!invite_hash.empty() && is_base64url_characters(invite_hash)) { // /+ return td::make_unique(PSTRING() << "tg:join?invite=" << url_encode(invite_hash)); } @@ -1431,14 +1476,14 @@ unique_ptr LinkManager::parse_t_me_link_query(Slice q } else if (path[0] == "share" || path[0] == "msg") { if (!(path.size() > 1 && (path[1] == "bookmarklet" || path[1] == "embed"))) { // /share?url= - // /share/url?url=&text= + // /share?url=&text= return get_internal_link_message_draft(get_arg("url"), get_arg("text")); } } else if (path[0] == "iv") { if (path.size() == 1 && has_arg("url")) { // /iv?url=&rhash= return td::make_unique( - PSTRING() << "https://t.me/iv" << copy_arg("url") << copy_arg("rhash"), get_arg("url")); + PSTRING() << get_t_me_url() << "iv" << copy_arg("url") << copy_arg("rhash"), get_arg("url")); } } else if (is_valid_username(path[0])) { if (path.size() >= 2 && to_integer(path[1]) > 0) { @@ -1456,6 +1501,8 @@ unique_ptr LinkManager::parse_t_me_link_query(Slice q << copy_arg("comment") << copy_arg("t")); } if (path.size() == 2 && is_valid_web_app_name(path[1])) { + // // + // //?startapp= return td::make_unique(path[0], path[1], url_query.get_arg("startapp").str()); } auto username = path[0]; @@ -1570,6 +1617,496 @@ unique_ptr LinkManager::get_internal_link_passport( callback_url.str()); } +Result LinkManager::get_internal_link(const td_api::object_ptr &type, + bool is_internal) { + if (type == nullptr) { + return Status::Error(400, "Link type must be non-empty"); + } + return get_internal_link_impl(type.get(), is_internal); +} + +Result LinkManager::get_internal_link_impl(const td_api::InternalLinkType *type_ptr, bool is_internal) { + switch (type_ptr->get_id()) { + case td_api::internalLinkTypeActiveSessions::ID: + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return "tg://settings/devices"; + case td_api::internalLinkTypeAttachmentMenuBot::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"); + } + start_parameter = PSTRING() << '=' << Slice(link->url_).substr(8); + } + if (link->target_chat_ == nullptr) { + return Status::Error(400, "Target chat must be non-empty"); + } + switch (link->target_chat_->get_id()) { + case td_api::targetChatChosen::ID: { + auto target = static_cast(link->target_chat_.get()); + if (!target->allow_user_chats_ && !target->allow_bot_chats_ && !target->allow_group_chats_ && + !target->allow_channel_chats_) { + return Status::Error(400, "At least one target chat type must be allowed"); + } + vector types; + if (target->allow_user_chats_) { + types.push_back("users"); + } + if (target->allow_bot_chats_) { + types.push_back("bots"); + } + if (target->allow_group_chats_) { + types.push_back("groups"); + } + if (target->allow_channel_chats_) { + types.push_back("channels"); + } + auto choose = implode(types, '+'); + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << link->bot_username_ << "&startattach" << start_parameter + << "&choose=" << choose; + } else { + return PSTRING() << get_t_me_url() << link->bot_username_ << "?startattach" << start_parameter + << "&choose=" << choose; + } + } + case td_api::targetChatCurrent::ID: + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << link->bot_username_ << "&startattach" << start_parameter; + } else { + return PSTRING() << get_t_me_url() << link->bot_username_ << "?startattach" << start_parameter; + } + case td_api::targetChatInternalLink::ID: { + auto target = static_cast(link->target_chat_.get()); + if (!start_parameter.empty()) { + start_parameter = "&startattach" + start_parameter; + } + if (target->link_ == nullptr || target->link_->get_id() != td_api::internalLinkTypePublicChat::ID) { + if (target->link_->get_id() == td_api::internalLinkTypeUserPhoneNumber::ID) { + auto user_phone_number_link = + static_cast(target->link_.get()); + if (!is_valid_phone_number(user_phone_number_link->phone_number_)) { + return Status::Error(400, "Invalid target phone number specified"); + } + if (is_internal) { + return PSTRING() << "tg://resolve?phone=" << user_phone_number_link->phone_number_ + << "&attach=" << link->bot_username_ << start_parameter; + } else { + return PSTRING() << get_t_me_url() << '+' << user_phone_number_link->phone_number_ + << "?attach=" << link->bot_username_ << start_parameter; + } + } + return Status::Error(400, "Unsupported target link specified"); + } + auto public_chat_link = static_cast(target->link_.get()); + if (!is_valid_username(public_chat_link->chat_username_)) { + return Status::Error(400, "Invalid target public chat username specified"); + } + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << public_chat_link->chat_username_ + << "&attach=" << link->bot_username_ << start_parameter; + } else { + return PSTRING() << get_t_me_url() << public_chat_link->chat_username_ << "?attach=" << link->bot_username_ + << start_parameter; + } + } + default: + UNREACHABLE(); + } + break; + } + case td_api::internalLinkTypeAuthenticationCode::ID: { + auto link = static_cast(type_ptr); + if (is_internal) { + return PSTRING() << "tg://login?code=" << url_encode(link->code_); + } else { + return PSTRING() << get_t_me_url() << "login/" << url_encode(link->code_); + } + } + case td_api::internalLinkTypeBackground::ID: { + auto link = static_cast(type_ptr); + auto params_pos = link->background_name_.find('?'); + string slug = params_pos >= link->background_name_.size() ? link->background_name_ + : link->background_name_.substr(0, params_pos); + if (slug.empty()) { + return Status::Error(400, "Background name must be non-empty"); + } + + if (BackgroundType::is_background_name_local(slug)) { + TRY_RESULT(background_type, BackgroundType::get_local_background_type(link->background_name_)); + auto background_link = background_type.get_link(!is_internal); + CHECK(!background_type.has_file()); + if (is_internal) { + Slice field_name = background_type.has_gradient_fill() ? Slice("gradient") : Slice("color"); + return PSTRING() << "tg://bg?" << field_name << '=' << background_link; + } else { + return PSTRING() << get_t_me_url() << "bg/" << background_link; + } + } + + auto prefix = is_internal ? string("tg://bg?slug=") : get_t_me_url() + "bg/"; + const auto url_query = parse_url_query(link->background_name_); + + bool is_first_arg = !is_internal; + auto copy_arg = [&](Slice name) { + return CopyArg(name, &url_query, &is_first_arg); + }; + return PSTRING() << prefix << url_encode(slug) << copy_arg("mode") << copy_arg("intensity") + << copy_arg("bg_color") << copy_arg("rotation"); + } + case td_api::internalLinkTypeBotAddToChannel::ID: { + auto link = static_cast(type_ptr); + if (!is_valid_username(link->bot_username_)) { + return Status::Error(400, "Invalid bot username specified"); + } + auto admin = get_admin_string(AdministratorRights(link->administrator_rights_, ChannelType::Broadcast)); + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << link->bot_username_ << "&startchannel" << admin; + } else { + return PSTRING() << get_t_me_url() << link->bot_username_ << "?startchannel" << admin; + } + } + case td_api::internalLinkTypeBotStart::ID: { + auto link = static_cast(type_ptr); + if (link->autostart_) { + return Status::Error(400, "Can't create an autostart bot link"); + } + if (!is_valid_username(link->bot_username_)) { + return Status::Error(400, "Invalid bot username specified"); + } + if (!is_valid_start_parameter(link->start_parameter_)) { + return Status::Error(400, "Invalid start parameter specified"); + } + auto start_parameter = link->start_parameter_.empty() ? string() : "=" + link->start_parameter_; + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << link->bot_username_ << "&start" << start_parameter; + } else { + return PSTRING() << get_t_me_url() << link->bot_username_ << "?start" << start_parameter; + } + } + case td_api::internalLinkTypeBotStartInGroup::ID: { + auto link = static_cast(type_ptr); + if (!is_valid_username(link->bot_username_)) { + return Status::Error(400, "Invalid bot username specified"); + } + if (!is_valid_start_parameter(link->start_parameter_)) { + return Status::Error(400, "Invalid start parameter specified"); + } + auto admin = get_admin_string(AdministratorRights(link->administrator_rights_, ChannelType::Megagroup)); + auto start_parameter = link->start_parameter_.empty() ? string() : "=" + link->start_parameter_; + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << link->bot_username_ << "&startgroup" << start_parameter << admin; + } else { + return PSTRING() << get_t_me_url() << link->bot_username_ << "?startgroup" << start_parameter << admin; + } + } + case td_api::internalLinkTypeChangePhoneNumber::ID: + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return "tg://settings/change_number"; + case td_api::internalLinkTypeChatInvite::ID: { + auto link = static_cast(type_ptr); + auto invite_hash = url_encode(get_dialog_invite_link_hash(link->invite_link_)); + if (invite_hash.empty()) { + return Status::Error(400, "Invalid invite link specified"); + } + return get_dialog_invite_link(invite_hash, is_internal); + } + case td_api::internalLinkTypeDefaultMessageAutoDeleteTimerSettings::ID: + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return "tg://settings/auto_delete"; + case td_api::internalLinkTypeEditProfileSettings::ID: + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return "tg://settings/edit_profile"; + case td_api::internalLinkTypeFilterSettings::ID: + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return "tg://settings/folders"; + case td_api::internalLinkTypeGame::ID: { + auto link = static_cast(type_ptr); + if (!is_valid_username(link->bot_username_)) { + return Status::Error(400, "Invalid bot username specified"); + } + if (!is_valid_game_name(link->game_short_name_)) { + return Status::Error(400, "Invalid game name specified"); + } + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << link->bot_username_ << "&game=" << link->game_short_name_; + } else { + return PSTRING() << get_t_me_url() << link->bot_username_ << "?game=" << link->game_short_name_; + } + } + case td_api::internalLinkTypeInstantView::ID: { + auto link = static_cast(type_ptr); + if (is_internal) { + return Status::Error("Deep link is unavailable for the link type"); + } + auto info = get_link_info(link->url_); + auto fallback_info = get_link_info(link->fallback_url_); + switch (info.type_) { + case LinkType::External: + case LinkType::Tg: + return Status::Error("Invalid instant view URL provided"); + case LinkType::Telegraph: + if (fallback_info.type_ != LinkType::Telegraph || + link->url_ != (PSLICE() << "https://telegra.ph" << fallback_info.query_)) { + return Status::Error("Unrelated fallback URL provided"); + } + return link->fallback_url_; + case LinkType::TMe: + // skip URL and fallback_url consistency checks + return link->url_; + default: + UNREACHABLE(); + break; + } + } + case td_api::internalLinkTypeInvoice::ID: { + auto link = static_cast(type_ptr); + if (is_internal) { + return PSTRING() << "tg://invoice?slug=" << url_encode(link->invoice_name_); + } else { + return PSTRING() << get_t_me_url() << '$' << url_encode(link->invoice_name_); + } + } + case td_api::internalLinkTypeLanguagePack::ID: { + auto link = static_cast(type_ptr); + if (is_internal) { + return PSTRING() << "tg://setlanguage?lang=" << url_encode(link->language_pack_id_); + } else { + return PSTRING() << get_t_me_url() << "setlanguage/" << url_encode(link->language_pack_id_); + } + } + case td_api::internalLinkTypeLanguageSettings::ID: + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return "tg://settings/language"; + case td_api::internalLinkTypeMessage::ID: { + auto link = static_cast(type_ptr); + auto parsed_link = parse_internal_link(link->url_); + if (parsed_link == nullptr) { + return Status::Error(400, "Invalid message URL specified"); + } + auto parsed_object = parsed_link->get_internal_link_type_object(); + if (parsed_object->get_id() != td_api::internalLinkTypeMessage::ID) { + return Status::Error(400, "Invalid message URL specified"); + } + if (!is_internal) { + return Status::Error(400, "Use getMessageLink to get HTTPS link to a message"); + } + return std::move(static_cast(*parsed_object).url_); + } + case td_api::internalLinkTypeMessageDraft::ID: { + auto link = static_cast(type_ptr); + string text; + if (link->text_ != nullptr) { + text = std::move(link->text_->text_); + } + string url; + if (link->contains_link_) { + std::tie(url, text) = split(text, '\n'); + } else { + url = std::move(text); + text.clear(); + } + if (!text.empty()) { + text = "&text=" + url_encode(text); + } + if (is_internal) { + return PSTRING() << "tg://msg_url?url=" << url_encode(url) << text; + } else { + return PSTRING() << get_t_me_url() << "share?url=" << url_encode(url) << text; + } + } + case td_api::internalLinkTypePassportDataRequest::ID: { + auto link = static_cast(type_ptr); + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + if (!UserId(link->bot_user_id_).is_valid()) { + return Status::Error("Invalid bot user identifier specified"); + } + return PSTRING() << "tg://resolve?domain=telegrampassport&bot_id=" << link->bot_user_id_ + << "&scope=" << url_encode(link->scope_) << "&public_key=" << url_encode(link->public_key_) + << "&nonce=" << url_encode(link->nonce_) << "&callback_url=" << url_encode(link->callback_url_); + } + case td_api::internalLinkTypePhoneNumberConfirmation::ID: { + auto link = static_cast(type_ptr); + if (is_internal) { + return PSTRING() << "tg://confirmphone?phone=" << url_encode(link->phone_number_) + << "&hash=" << url_encode(link->hash_); + } else { + return PSTRING() << get_t_me_url() << "confirmphone?phone=" << url_encode(link->phone_number_) + << "&hash=" << url_encode(link->hash_); + } + } + case td_api::internalLinkTypePremiumFeatures::ID: { + auto link = static_cast(type_ptr); + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return PSTRING() << "tg://premium_offer?ref=" << url_encode(link->referrer_); + } + case td_api::internalLinkTypePrivacyAndSecuritySettings::ID: + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return "tg://settings/privacy"; + case td_api::internalLinkTypeProxy::ID: { + auto link = static_cast(type_ptr); + TRY_RESULT(proxy, Proxy::create_proxy(link->server_, link->port_, link->type_.get())); + return get_proxy_link(proxy, is_internal); + } + case td_api::internalLinkTypePublicChat::ID: { + auto link = static_cast(type_ptr); + if (!is_valid_username(link->chat_username_)) { + return Status::Error(400, "Invalid chat username specified"); + } + return get_public_dialog_link(link->chat_username_, is_internal); + } + case td_api::internalLinkTypeQrCodeAuthentication::ID: + return Status::Error("The link must never be generated client-side"); + case td_api::internalLinkTypeRestorePurchases::ID: + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return "tg://restore_purchases"; + case td_api::internalLinkTypeSettings::ID: + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return "tg://settings"; + case td_api::internalLinkTypeStickerSet::ID: { + auto link = static_cast(type_ptr); + if (link->sticker_set_name_.empty()) { + return Status::Error(400, "Invalid sticker set name specified"); + } + if (is_internal) { + return PSTRING() << "tg://add" << (link->expect_custom_emoji_ ? "emoji" : "stickers") + << "?set=" << url_encode(link->sticker_set_name_); + } else { + return PSTRING() << get_t_me_url() << "add" << (link->expect_custom_emoji_ ? "emoji" : "stickers") << '/' + << url_encode(link->sticker_set_name_); + } + } + case td_api::internalLinkTypeTheme::ID: { + auto link = static_cast(type_ptr); + if (link->theme_name_.empty()) { + return Status::Error(400, "Invalid theme name specified"); + } + if (is_internal) { + return PSTRING() << "tg://addtheme?slug=" << url_encode(link->theme_name_); + } else { + return PSTRING() << get_t_me_url() << "addtheme/" << url_encode(link->theme_name_); + } + } + case td_api::internalLinkTypeThemeSettings::ID: + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + return "tg://settings/themes"; + case td_api::internalLinkTypeUnknownDeepLink::ID: { + auto link = static_cast(type_ptr); + if (!is_internal) { + return Status::Error("HTTP link is unavailable for the link type"); + } + auto parsed_link = parse_internal_link(link->link_); + if (parsed_link == nullptr) { + return Status::Error(400, "Invalid deep link URL specified"); + } + auto parsed_object = parsed_link->get_internal_link_type_object(); + if (parsed_object->get_id() != td_api::internalLinkTypeUnknownDeepLink::ID) { + return Status::Error(400, "Invalid deep link URL specified"); + } + return std::move(static_cast(*parsed_object).link_); + } + case td_api::internalLinkTypeUnsupportedProxy::ID: + if (is_internal) { + return "tg://proxy?port=-1&server=0.0.0.0"; + } else { + return PSTRING() << get_t_me_url() << "proxy?port=-1&server=0.0.0.0"; + } + case td_api::internalLinkTypeUserPhoneNumber::ID: { + auto link = static_cast(type_ptr); + if (!is_valid_phone_number(link->phone_number_)) { + return Status::Error(400, "Invalid phone number specified"); + } + if (is_internal) { + return PSTRING() << "tg://resolve?phone=" << link->phone_number_; + } else { + return PSTRING() << get_t_me_url() << '+' << link->phone_number_; + } + } + case td_api::internalLinkTypeUserToken::ID: { + auto link = static_cast(type_ptr); + if (link->token_.empty()) { + return Status::Error(400, "Invalid user token specified"); + } + if (is_internal) { + return PSTRING() << "tg://contact?token=" << link->token_; + } else { + return PSTRING() << get_t_me_url() << "contact/" << link->token_; + } + } + case td_api::internalLinkTypeVideoChat::ID: { + auto link = static_cast(type_ptr); + if (!is_valid_username(link->chat_username_)) { + return Status::Error(400, "Invalid chat username specified"); + } + string invite_hash; + if (!link->invite_hash_.empty()) { + invite_hash = '=' + url_encode(link->invite_hash_); + } + auto name = link->is_live_stream_ ? Slice("livestream") : Slice("videochat"); + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << link->chat_username_ << '?' << name << invite_hash; + } else { + return PSTRING() << get_t_me_url() << link->chat_username_ << '?' << name << invite_hash; + } + } + case td_api::internalLinkTypeWebApp::ID: { + auto link = static_cast(type_ptr); + if (!is_valid_username(link->bot_username_)) { + return Status::Error(400, "Invalid bot username specified"); + } + if (!is_valid_web_app_name(link->web_app_short_name_)) { + return Status::Error(400, "Invalid Web App name specified"); + } + if (!is_valid_start_parameter(link->start_parameter_)) { + return Status::Error(400, "Invalid start parameter specified"); + } + string start_parameter; + if (!link->start_parameter_.empty()) { + start_parameter = PSTRING() << (is_internal ? '&' : '?') << "startapp=" << url_encode(start_parameter); + } + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << link->bot_username_ << "&appname=" << link->web_app_short_name_ + << start_parameter; + } else { + return PSTRING() << get_t_me_url() << link->bot_username_ << '/' << link->web_app_short_name_ + << start_parameter; + } + } + default: + break; + } + UNREACHABLE(); + return Status::Error(500, "Unsupported"); +} + void LinkManager::update_autologin_token(string autologin_token) { autologin_update_time_ = Time::now(); autologin_token_ = std::move(autologin_token); @@ -1719,6 +2256,9 @@ string LinkManager::get_dialog_invite_link_hash(Slice invite_link) { if (is_valid_phone_number(invite_hash)) { return string(); } + if (!is_base64url_characters(invite_hash)) { + return string(); + } return invite_hash; } @@ -1763,8 +2303,12 @@ string LinkManager::get_instant_view_link(Slice url, Slice rhash) { return PSTRING() << get_t_me_url() << "iv?url=" << url_encode(url) << "&rhash=" << url_encode(rhash); } -string LinkManager::get_public_chat_link(Slice username) { - return PSTRING() << get_t_me_url() << url_encode(username); +string LinkManager::get_public_dialog_link(Slice username, bool is_internal) { + if (is_internal) { + return PSTRING() << "tg://resolve?domain=" << url_encode(username); + } else { + return PSTRING() << get_t_me_url() << url_encode(username); + } } Result LinkManager::get_proxy_link(const Proxy &proxy, bool is_internal) { diff --git a/td/telegram/LinkManager.h b/td/telegram/LinkManager.h index e43ba0d6d..501622736 100644 --- a/td/telegram/LinkManager.h +++ b/td/telegram/LinkManager.h @@ -60,6 +60,8 @@ class LinkManager final : public Actor { // checks whether the link is a supported tg or t.me link and parses it static unique_ptr parse_internal_link(Slice link, bool is_trusted = false); + static Result get_internal_link(const td_api::object_ptr &type, bool is_internal); + void update_autologin_token(string autologin_token); void update_autologin_domains(vector autologin_domains, vector url_auth_domains, @@ -91,7 +93,7 @@ class LinkManager final : public Actor { static string get_instant_view_link(Slice url, Slice rhash); - static string get_public_chat_link(Slice username); + static string get_public_dialog_link(Slice username, bool is_internal); static Result get_proxy_link(const Proxy &proxy, bool is_internal); @@ -164,6 +166,8 @@ class LinkManager final : public Actor { static unique_ptr get_internal_link_message_draft(Slice url, Slice text); + static Result get_internal_link_impl(const td_api::InternalLinkType *type_ptr, bool is_internal); + static Result check_link_impl(Slice link, bool http_only, bool https_only); Td *td_; diff --git a/td/telegram/Td.cpp b/td/telegram/Td.cpp index 9630ccfdb..a6be3a31d 100644 --- a/td/telegram/Td.cpp +++ b/td/telegram/Td.cpp @@ -2824,6 +2824,7 @@ bool Td::is_preinitialization_request(int32 id) { bool Td::is_preauthentication_request(int32 id) { switch (id) { + case td_api::getInternalLink::ID: case td_api::getInternalLinkType::ID: case td_api::getLocalizationTargetInfo::ID: case td_api::getLanguagePackInfo::ID: @@ -5117,6 +5118,15 @@ void Td::on_request(uint64 id, const td_api::clickAnimatedEmojiMessage &request) std::move(promise)); } +void Td::on_request(uint64 id, const td_api::getInternalLink &request) { + auto r_link = LinkManager::get_internal_link(request.type_, !request.is_http_); + if (r_link.is_error()) { + send_closure(actor_id(this), &Td::send_error, id, r_link.move_as_error()); + } else { + send_closure(actor_id(this), &Td::send_result, id, td_api::make_object(r_link.move_as_ok())); + } +} + void Td::on_request(uint64 id, const td_api::getInternalLinkType &request) { auto type = LinkManager::parse_internal_link(request.link_); send_closure(actor_id(this), &Td::send_result, id, type == nullptr ? nullptr : type->get_internal_link_type_object()); diff --git a/td/telegram/Td.h b/td/telegram/Td.h index 04fe748ba..63479e057 100644 --- a/td/telegram/Td.h +++ b/td/telegram/Td.h @@ -644,6 +644,8 @@ class Td final : public Actor { void on_request(uint64 id, const td_api::clickAnimatedEmojiMessage &request); + void on_request(uint64 id, const td_api::getInternalLink &request); + void on_request(uint64 id, const td_api::getInternalLinkType &request); void on_request(uint64 id, td_api::getExternalLinkInfo &request); diff --git a/td/telegram/net/Proxy.cpp b/td/telegram/net/Proxy.cpp index 9f239c1d4..2b8249431 100644 --- a/td/telegram/net/Proxy.cpp +++ b/td/telegram/net/Proxy.cpp @@ -10,7 +10,7 @@ namespace td { -Result Proxy::create_proxy(string server, int port, td_api::ProxyType *proxy_type) { +Result Proxy::create_proxy(string server, int port, const td_api::ProxyType *proxy_type) { if (proxy_type == nullptr) { return Status::Error(400, "Proxy type must be non-empty"); } @@ -26,19 +26,19 @@ Result Proxy::create_proxy(string server, int port, td_api::ProxyType *pr switch (proxy_type->get_id()) { case td_api::proxyTypeSocks5::ID: { - auto type = static_cast(proxy_type); - return Proxy::socks5(std::move(server), port, std::move(type->username_), std::move(type->password_)); + auto type = static_cast(proxy_type); + return Proxy::socks5(std::move(server), port, type->username_, type->password_); } case td_api::proxyTypeHttp::ID: { - auto type = static_cast(proxy_type); + auto type = static_cast(proxy_type); if (type->http_only_) { - return Proxy::http_caching(std::move(server), port, std::move(type->username_), std::move(type->password_)); + return Proxy::http_caching(std::move(server), port, type->username_, type->password_); } else { - return Proxy::http_tcp(std::move(server), port, std::move(type->username_), std::move(type->password_)); + return Proxy::http_tcp(std::move(server), port, type->username_, type->password_); } } case td_api::proxyTypeMtproto::ID: { - auto type = static_cast(proxy_type); + auto type = static_cast(proxy_type); TRY_RESULT(secret, mtproto::ProxySecret::from_link(type->secret_)); return Proxy::mtproto(std::move(server), port, std::move(secret)); } diff --git a/td/telegram/net/Proxy.h b/td/telegram/net/Proxy.h index ce5952313..517822b63 100644 --- a/td/telegram/net/Proxy.h +++ b/td/telegram/net/Proxy.h @@ -22,7 +22,7 @@ class ProxyType; class Proxy { public: - static Result create_proxy(string server, int port, td_api::ProxyType *proxy_type); + static Result create_proxy(string server, int port, const td_api::ProxyType *proxy_type); static Proxy socks5(string server, int32 port, string user, string password) { Proxy proxy; diff --git a/test/link.cpp b/test/link.cpp index 9cf9092b7..6032b9a81 100644 --- a/test/link.cpp +++ b/test/link.cpp @@ -89,6 +89,44 @@ static void parse_internal_link(const td::string &url, td::td_api::object_ptr(object.get())->text_->entities_.clear(); } ASSERT_STREQ(url + ' ' + to_string(expected), url + ' ' + to_string(object)); + + for (auto is_internal : {true, false}) { + if (!is_internal && expected->get_id() == td::td_api::internalLinkTypeMessage::ID) { + // external message links must be generated with getMessageLink + continue; + } + if (expected->get_id() == td::td_api::internalLinkTypeQrCodeAuthentication::ID) { + // QR code authentication links must never be generated manually + continue; + } + auto r_link = td::LinkManager::get_internal_link(expected, is_internal); + if (r_link.is_error()) { + if (r_link.error().message() == "HTTP link is unavailable for the link type") { + // some links are tg-only + continue; + } + if (r_link.error().message() == "Deep link is unavailable for the link type") { + // some links are http-only + continue; + } + if (r_link.error().message() == "WALLPAPER_INVALID") { + continue; + } + LOG(ERROR) << url << ' ' << r_link.error() << ' ' << to_string(expected); + ASSERT_TRUE(r_link.is_ok()); + } + auto new_result = td::LinkManager::parse_internal_link(r_link.ok()); + ASSERT_TRUE(new_result != nullptr); + auto new_object = new_result->get_internal_link_type_object(); + + r_link = td::LinkManager::get_internal_link(new_object, is_internal); + ASSERT_TRUE(r_link.is_ok()); + new_result = td::LinkManager::parse_internal_link(r_link.ok()); + ASSERT_TRUE(new_result != nullptr); + + // the object must be the same after 2 round of conversion + ASSERT_STREQ(to_string(new_object), to_string(new_result->get_internal_link_type_object())); + } } else { LOG_IF(ERROR, expected != nullptr) << url; ASSERT_TRUE(expected == nullptr); @@ -568,7 +606,7 @@ TEST(Link, parse_internal_link) { parse_internal_link("t.me/joinchat/?abcdef", nullptr); parse_internal_link("t.me/joinchat/#abcdef", nullptr); parse_internal_link("t.me/joinchat/abacaba", chat_invite("abacaba")); - parse_internal_link("t.me/joinchat/aba%20aba", chat_invite("aba%20aba")); + parse_internal_link("t.me/joinchat/aba%20aba", nullptr); parse_internal_link("t.me/joinchat/aba%30aba", chat_invite("aba0aba")); parse_internal_link("t.me/joinchat/123456a", chat_invite("123456a")); parse_internal_link("t.me/joinchat/12345678901", nullptr); @@ -586,7 +624,7 @@ TEST(Link, parse_internal_link) { parse_internal_link("t.me/+?abcdef", nullptr); parse_internal_link("t.me/+#abcdef", nullptr); parse_internal_link("t.me/ abacaba", chat_invite("abacaba")); - parse_internal_link("t.me/+aba%20aba", chat_invite("aba%20aba")); + parse_internal_link("t.me/+aba%20aba", nullptr); parse_internal_link("t.me/+aba%30aba", chat_invite("aba0aba")); parse_internal_link("t.me/+123456a", chat_invite("123456a")); parse_internal_link("t.me/%2012345678901", user_phone_number("12345678901")); @@ -611,7 +649,7 @@ TEST(Link, parse_internal_link) { parse_internal_link("t.me/contact/?attach=&startattach", nullptr); parse_internal_link("tg:join?invite=abcdef", chat_invite("abcdef")); - parse_internal_link("tg:join?invite=abc%20def", chat_invite("abc%20def")); + parse_internal_link("tg:join?invite=abc%20def", unknown_deep_link("tg://join?invite=abc%20def")); parse_internal_link("tg://join?invite=abc%30def", chat_invite("abc0def")); parse_internal_link("tg:join?invite=", unknown_deep_link("tg://join?invite="));