// // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2023 // // 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/LinkManager.h" #include "td/telegram/AccessRights.h" #include "td/telegram/BackgroundType.h" #include "td/telegram/ChannelId.h" #include "td/telegram/ChannelType.h" #include "td/telegram/ConfigManager.h" #include "td/telegram/ContactsManager.h" #include "td/telegram/DialogId.h" #include "td/telegram/DialogParticipant.h" #include "td/telegram/Global.h" #include "td/telegram/MessageEntity.h" #include "td/telegram/MessageId.h" #include "td/telegram/MessagesManager.h" #include "td/telegram/misc.h" #include "td/telegram/net/Proxy.h" #include "td/telegram/ServerMessageId.h" #include "td/telegram/Td.h" #include "td/telegram/TdDb.h" #include "td/telegram/telegram_api.h" #include "td/telegram/UserId.h" #include "td/mtproto/ProxySecret.h" #include "td/utils/algorithm.h" #include "td/utils/base64.h" #include "td/utils/buffer.h" #include "td/utils/HttpUrl.h" #include "td/utils/logging.h" #include "td/utils/misc.h" #include "td/utils/SliceBuilder.h" #include "td/utils/StringBuilder.h" #include "td/utils/Time.h" #include "td/utils/utf8.h" #include namespace td { static bool is_valid_start_parameter(Slice start_parameter) { return start_parameter.size() <= 64 && is_base64url_characters(start_parameter); } static bool is_valid_phone_number(Slice phone_number) { if (phone_number.empty() || phone_number.size() > 32) { return false; } for (auto c : phone_number) { if (!is_digit(c)) { return false; } } return true; } static bool is_valid_game_name(Slice name) { return name.size() >= 3 && is_valid_username(name); } static bool is_valid_web_app_name(Slice name) { return name.size() >= 3 && is_valid_username(name); } static string get_url_query_hash(bool is_tg, const HttpUrlQuery &url_query) { const auto &path = url_query.path_; if (is_tg) { if (path.size() == 1 && path[0] == "join" && !url_query.get_arg("invite").empty()) { // join?invite=abcdef return url_query.get_arg("invite").str(); } } else { if (path.size() >= 2 && path[0] == "joinchat" && !path[1].empty()) { // /joinchat/ return path[1]; } if (!path.empty() && path[0].size() >= 2 && (path[0][0] == ' ' || path[0][0] == '+')) { // /+ return path[0].substr(1); } } return string(); } static AdministratorRights get_administrator_rights(Slice rights, bool for_channel) { bool can_manage_dialog = false; bool can_change_info = false; bool can_post_messages = false; bool can_edit_messages = false; bool can_delete_messages = false; bool can_invite_users = false; bool can_restrict_members = false; bool can_pin_messages = false; bool can_manage_topics = false; bool can_promote_members = false; bool can_manage_calls = false; bool is_anonymous = false; for (auto right : full_split(rights, ' ')) { if (right == "change_info") { can_change_info = true; } else if (right == "post_messages") { can_post_messages = true; } else if (right == "edit_messages") { can_edit_messages = true; } else if (right == "delete_messages") { can_delete_messages = true; } else if (right == "restrict_members") { can_restrict_members = true; } else if (right == "invite_users") { can_invite_users = true; } else if (right == "pin_messages") { can_pin_messages = true; } else if (right == "manage_topics") { can_manage_topics = true; } else if (right == "promote_members") { can_promote_members = true; } else if (right == "manage_video_chats") { can_manage_calls = true; } else if (right == "anonymous") { is_anonymous = true; } else if (right == "manage_chat") { can_manage_dialog = true; } } return AdministratorRights(is_anonymous, can_manage_dialog, can_change_info, can_post_messages, can_edit_messages, can_delete_messages, can_invite_users, can_restrict_members, can_pin_messages, can_manage_topics, can_promote_members, can_manage_calls, 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; bool allow_groups = false; bool allow_channels = false; for (auto chat_type : full_split(chat_types, ' ')) { if (chat_type == "users") { allow_users = true; } else if (chat_type == "bots") { allow_bots = true; } else if (chat_type == "groups") { allow_groups = true; } else if (chat_type == "channels") { allow_channels = true; } } if (!allow_users && !allow_bots && !allow_groups && !allow_channels) { return nullptr; } return td_api::make_object(allow_users, allow_bots, allow_groups, allow_channels); } class LinkManager::InternalLinkActiveSessions final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkAttachMenuBot final : public InternalLink { td_api::object_ptr allowed_chat_types_; unique_ptr dialog_link_; string bot_username_; string url_; td_api::object_ptr get_internal_link_type_object() const final { td_api::object_ptr target_chat; if (dialog_link_ != nullptr) { target_chat = td_api::make_object(dialog_link_->get_internal_link_type_object()); } else if (allowed_chat_types_ != nullptr) { target_chat = td_api::make_object( allowed_chat_types_->allow_user_chats_, allowed_chat_types_->allow_bot_chats_, allowed_chat_types_->allow_group_chats_, allowed_chat_types_->allow_channel_chats_); } else { target_chat = td_api::make_object(); } return td_api::make_object(std::move(target_chat), bot_username_, url_); } public: InternalLinkAttachMenuBot(td_api::object_ptr allowed_chat_types, unique_ptr dialog_link, string bot_username, Slice start_parameter) : allowed_chat_types_(std::move(allowed_chat_types)) , dialog_link_(std::move(dialog_link)) , bot_username_(std::move(bot_username)) { if (!start_parameter.empty()) { url_ = PSTRING() << "start://" << start_parameter; } } }; class LinkManager::InternalLinkAuthenticationCode final : public InternalLink { string code_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(code_); } public: explicit InternalLinkAuthenticationCode(string code) : code_(std::move(code)) { } }; class LinkManager::InternalLinkBackground final : public InternalLink { string background_name_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(background_name_); } public: explicit InternalLinkBackground(string background_name) : background_name_(std::move(background_name)) { } }; class LinkManager::InternalLinkBotAddToChannel final : public InternalLink { string bot_username_; AdministratorRights administrator_rights_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object( bot_username_, administrator_rights_.get_chat_administrator_rights_object()); } public: InternalLinkBotAddToChannel(string bot_username, AdministratorRights &&administrator_rights) : bot_username_(std::move(bot_username)), administrator_rights_(std::move(administrator_rights)) { } }; class LinkManager::InternalLinkBotStart final : public InternalLink { string bot_username_; string start_parameter_; bool autostart_; td_api::object_ptr get_internal_link_type_object() const final { bool autostart = autostart_; if (Scheduler::context() != nullptr && bot_username_ == G()->get_option_string("premium_bot_username")) { autostart = true; } return td_api::make_object(bot_username_, start_parameter_, autostart); } public: InternalLinkBotStart(string bot_username, string start_parameter, bool autostart) : bot_username_(std::move(bot_username)), start_parameter_(std::move(start_parameter)), autostart_(autostart) { } }; class LinkManager::InternalLinkBotStartInGroup final : public InternalLink { string bot_username_; string start_parameter_; AdministratorRights administrator_rights_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object( bot_username_, start_parameter_, administrator_rights_ == AdministratorRights() ? nullptr : administrator_rights_.get_chat_administrator_rights_object()); } public: InternalLinkBotStartInGroup(string bot_username, string start_parameter, AdministratorRights &&administrator_rights) : bot_username_(std::move(bot_username)) , start_parameter_(std::move(start_parameter)) , administrator_rights_(std::move(administrator_rights)) { } }; class LinkManager::InternalLinkChangePhoneNumber final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkConfirmPhone final : public InternalLink { string hash_; string phone_number_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(hash_, phone_number_); } public: InternalLinkConfirmPhone(string hash, string phone_number) : hash_(std::move(hash)), phone_number_(std::move(phone_number)) { } }; class LinkManager::InternalLinkDefaultMessageAutoDeleteTimerSettings final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkDialogInvite final : public InternalLink { string url_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(url_); } public: explicit InternalLinkDialogInvite(string url) : url_(std::move(url)) { } }; class LinkManager::InternalLinkEditProfileSettings final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkFilterSettings final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkGame final : public InternalLink { string bot_username_; string game_short_name_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(bot_username_, game_short_name_); } public: InternalLinkGame(string bot_username, string game_short_name) : bot_username_(std::move(bot_username)), game_short_name_(std::move(game_short_name)) { } }; class LinkManager::InternalLinkInstantView final : public InternalLink { string url_; string fallback_url_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(url_, fallback_url_); } public: InternalLinkInstantView(string url, string fallback_url) : url_(std::move(url)), fallback_url_(std::move(fallback_url)) { } }; class LinkManager::InternalLinkInvoice final : public InternalLink { string invoice_name_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(invoice_name_); } public: explicit InternalLinkInvoice(string invoice_name) : invoice_name_(std::move(invoice_name)) { } }; class LinkManager::InternalLinkLanguage final : public InternalLink { string language_pack_id_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(language_pack_id_); } public: explicit InternalLinkLanguage(string language_pack_id) : language_pack_id_(std::move(language_pack_id)) { } }; class LinkManager::InternalLinkLanguageSettings final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkMessage final : public InternalLink { string url_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(url_); } public: explicit InternalLinkMessage(string url) : url_(std::move(url)) { } }; class LinkManager::InternalLinkMessageDraft final : public InternalLink { FormattedText text_; bool contains_link_ = false; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(get_formatted_text_object(text_, true, -1), contains_link_); } public: InternalLinkMessageDraft(FormattedText &&text, bool contains_link) : text_(std::move(text)), contains_link_(contains_link) { } }; class LinkManager::InternalLinkPassportDataRequest final : public InternalLink { UserId bot_user_id_; string scope_; string public_key_; string nonce_; string callback_url_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(bot_user_id_.get(), scope_, public_key_, nonce_, callback_url_); } public: InternalLinkPassportDataRequest(UserId bot_user_id, string scope, string public_key, string nonce, string callback_url) : bot_user_id_(bot_user_id) , scope_(std::move(scope)) , public_key_(std::move(public_key)) , nonce_(std::move(nonce)) , callback_url_(std::move(callback_url)) { } }; class LinkManager::InternalLinkPremiumFeatures final : public InternalLink { string referrer_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(referrer_); } public: explicit InternalLinkPremiumFeatures(string referrer) : referrer_(std::move(referrer)) { } }; class LinkManager::InternalLinkPrivacyAndSecuritySettings final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkProxy final : public InternalLink { string server_; int32 port_; td_api::object_ptr type_; td_api::object_ptr get_internal_link_type_object() const final { CHECK(type_ != nullptr); auto type = type_.get(); auto proxy_type = [type]() -> td_api::object_ptr { switch (type->get_id()) { case td_api::proxyTypeSocks5::ID: { auto type_socks = static_cast(type); return td_api::make_object(type_socks->username_, type_socks->password_); } case td_api::proxyTypeMtproto::ID: { auto type_mtproto = static_cast(type); return td_api::make_object(type_mtproto->secret_); } default: UNREACHABLE(); return nullptr; } }(); return td_api::make_object(server_, port_, std::move(proxy_type)); } public: InternalLinkProxy(string server, int32 port, td_api::object_ptr type) : server_(std::move(server)), port_(port), type_(std::move(type)) { } }; class LinkManager::InternalLinkPublicDialog final : public InternalLink { string dialog_username_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(dialog_username_); } public: explicit InternalLinkPublicDialog(string dialog_username) : dialog_username_(std::move(dialog_username)) { } }; class LinkManager::InternalLinkQrCodeAuthentication final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkRestorePurchases final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkSettings final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkStickerSet final : public InternalLink { string sticker_set_name_; bool expect_custom_emoji_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(sticker_set_name_, expect_custom_emoji_); } public: InternalLinkStickerSet(string sticker_set_name, bool expect_custom_emoji) : sticker_set_name_(std::move(sticker_set_name)), expect_custom_emoji_(expect_custom_emoji) { } }; class LinkManager::InternalLinkTheme final : public InternalLink { string theme_name_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(theme_name_); } public: explicit InternalLinkTheme(string theme_name) : theme_name_(std::move(theme_name)) { } }; class LinkManager::InternalLinkThemeSettings final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkUnknownDeepLink final : public InternalLink { string link_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(link_); } public: explicit InternalLinkUnknownDeepLink(string link) : link_(std::move(link)) { } }; class LinkManager::InternalLinkUnsupportedProxy final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkUserPhoneNumber final : public InternalLink { string phone_number_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(phone_number_); } public: explicit InternalLinkUserPhoneNumber(string phone_number) : phone_number_(std::move(phone_number)) { } }; class LinkManager::InternalLinkUserToken final : public InternalLink { string token_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(token_); } public: explicit InternalLinkUserToken(string token) : token_(std::move(token)) { } }; class LinkManager::InternalLinkVoiceChat final : public InternalLink { string dialog_username_; string invite_hash_; bool is_live_stream_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(dialog_username_, invite_hash_, is_live_stream_); } public: InternalLinkVoiceChat(string dialog_username, string invite_hash, bool is_live_stream) : dialog_username_(std::move(dialog_username)) , invite_hash_(std::move(invite_hash)) , is_live_stream_(is_live_stream) { } }; class LinkManager::InternalLinkWebApp final : public InternalLink { string bot_username_; string web_app_short_name_; string start_parameter_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(bot_username_, web_app_short_name_, start_parameter_); } public: InternalLinkWebApp(string bot_username, string web_app_short_name, string start_parameter) : bot_username_(std::move(bot_username)) , web_app_short_name_(std::move(web_app_short_name)) , start_parameter_(std::move(start_parameter)) { } }; class GetDeepLinkInfoQuery final : public Td::ResultHandler { Promise> promise_; public: explicit GetDeepLinkInfoQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(Slice link) { send_query(G()->net_query_creator().create_unauth(telegram_api::help_getDeepLinkInfo(link.str()))); } 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(); switch (result->get_id()) { case telegram_api::help_deepLinkInfoEmpty::ID: return promise_.set_value(nullptr); case telegram_api::help_deepLinkInfo::ID: { auto info = telegram_api::move_object_as(result); auto entities = get_message_entities(nullptr, std::move(info->entities_), "GetDeepLinkInfoQuery"); auto status = fix_formatted_text(info->message_, entities, true, true, true, true, true); if (status.is_error()) { LOG(ERROR) << "Receive error " << status << " while parsing deep link info " << info->message_; if (!clean_input_string(info->message_)) { info->message_.clear(); } entities = find_entities(info->message_, true, true); } FormattedText text{std::move(info->message_), std::move(entities)}; return promise_.set_value( td_api::make_object(get_formatted_text_object(text, true, -1), info->update_app_)); } default: UNREACHABLE(); } } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; class RequestUrlAuthQuery final : public Td::ResultHandler { Promise> promise_; string url_; DialogId dialog_id_; public: explicit RequestUrlAuthQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(string url, FullMessageId full_message_id, int32 button_id) { url_ = std::move(url); int32 flags = 0; tl_object_ptr input_peer; if (full_message_id.get_dialog_id().is_valid()) { dialog_id_ = full_message_id.get_dialog_id(); input_peer = td_->messages_manager_->get_input_peer(dialog_id_, AccessRights::Read); CHECK(input_peer != nullptr); flags |= telegram_api::messages_requestUrlAuth::PEER_MASK; } else { flags |= telegram_api::messages_requestUrlAuth::URL_MASK; } send_query(G()->net_query_creator().create(telegram_api::messages_requestUrlAuth( flags, std::move(input_peer), full_message_id.get_message_id().get_server_message_id().get(), button_id, 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()); } auto result = result_ptr.move_as_ok(); LOG(INFO) << "Receive result for RequestUrlAuthQuery: " << to_string(result); switch (result->get_id()) { case telegram_api::urlAuthResultRequest::ID: { auto request = telegram_api::move_object_as(result); UserId bot_user_id = ContactsManager::get_user_id(request->bot_); if (!bot_user_id.is_valid()) { return on_error(Status::Error(500, "Receive invalid bot_user_id")); } td_->contacts_manager_->on_get_user(std::move(request->bot_), "RequestUrlAuthQuery"); promise_.set_value(td_api::make_object( url_, request->domain_, td_->contacts_manager_->get_user_id_object(bot_user_id, "RequestUrlAuthQuery"), request->request_write_access_)); break; } case telegram_api::urlAuthResultAccepted::ID: { auto accepted = telegram_api::move_object_as(result); promise_.set_value(td_api::make_object(accepted->url_, true)); break; } case telegram_api::urlAuthResultDefault::ID: promise_.set_value(td_api::make_object(url_, false)); break; } } void on_error(Status status) final { if (!dialog_id_.is_valid() || !td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "RequestUrlAuthQuery")) { LOG(INFO) << "Receive error for RequestUrlAuthQuery: " << status; } promise_.set_value(td_api::make_object(url_, false)); } }; class AcceptUrlAuthQuery final : public Td::ResultHandler { Promise> promise_; string url_; DialogId dialog_id_; public: explicit AcceptUrlAuthQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(string url, FullMessageId full_message_id, int32 button_id, bool allow_write_access) { url_ = std::move(url); int32 flags = 0; tl_object_ptr input_peer; if (full_message_id.get_dialog_id().is_valid()) { dialog_id_ = full_message_id.get_dialog_id(); input_peer = td_->messages_manager_->get_input_peer(dialog_id_, AccessRights::Read); CHECK(input_peer != nullptr); flags |= telegram_api::messages_acceptUrlAuth::PEER_MASK; } else { flags |= telegram_api::messages_acceptUrlAuth::URL_MASK; } if (allow_write_access) { flags |= telegram_api::messages_acceptUrlAuth::WRITE_ALLOWED_MASK; } send_query(G()->net_query_creator().create(telegram_api::messages_acceptUrlAuth( flags, false /*ignored*/, std::move(input_peer), full_message_id.get_message_id().get_server_message_id().get(), button_id, 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()); } auto result = result_ptr.move_as_ok(); LOG(INFO) << "Receive " << to_string(result); switch (result->get_id()) { case telegram_api::urlAuthResultRequest::ID: LOG(ERROR) << "Receive unexpected " << to_string(result); return on_error(Status::Error(500, "Receive unexpected urlAuthResultRequest")); case telegram_api::urlAuthResultAccepted::ID: { auto accepted = telegram_api::move_object_as(result); promise_.set_value(td_api::make_object(accepted->url_)); break; } case telegram_api::urlAuthResultDefault::ID: promise_.set_value(td_api::make_object(url_)); break; } } void on_error(Status status) final { if (!dialog_id_.is_valid() || !td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "AcceptUrlAuthQuery")) { LOG(INFO) << "Receive error for AcceptUrlAuthQuery: " << status; } promise_.set_error(std::move(status)); } }; LinkManager::LinkManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) { } LinkManager::~LinkManager() = default; void LinkManager::start_up() { autologin_update_time_ = Time::now() - 365 * 86400; autologin_domains_ = full_split(G()->td_db()->get_binlog_pmc()->get("autologin_domains"), '\xFF'); url_auth_domains_ = full_split(G()->td_db()->get_binlog_pmc()->get("url_auth_domains"), '\xFF'); whitelisted_domains_ = full_split(G()->td_db()->get_binlog_pmc()->get("whitelisted_domains"), '\xFF'); } void LinkManager::tear_down() { parent_.reset(); } static bool tolower_begins_with(Slice str, Slice prefix) { if (prefix.size() > str.size()) { return false; } for (size_t i = 0; i < prefix.size(); i++) { if (to_lower(str[i]) != prefix[i]) { return false; } } return true; } Result LinkManager::check_link(CSlice link, bool http_only, bool https_only) { auto result = check_link_impl(link, http_only, https_only); if (result.is_ok()) { return result; } auto error = result.move_as_error(); if (check_utf8(link)) { return Status::Error(400, PSLICE() << "URL '" << link << "' is invalid: " << error.message()); } else { return Status::Error(400, PSLICE() << "URL is invalid: " << error.message()); } } string LinkManager::get_checked_link(Slice link, bool http_only, bool https_only) { auto result = check_link_impl(link, http_only, https_only); if (result.is_ok()) { return result.move_as_ok(); } return string(); } Result LinkManager::check_link_impl(Slice link, bool http_only, bool https_only) { bool is_tg = false; bool is_ton = false; if (tolower_begins_with(link, "tg:")) { link.remove_prefix(3); is_tg = true; } else if (tolower_begins_with(link, "ton:")) { link.remove_prefix(4); is_ton = true; } if ((is_tg || is_ton) && begins_with(link, "//")) { link.remove_prefix(2); } TRY_RESULT(http_url, parse_url(link)); if (https_only && (http_url.protocol_ != HttpUrl::Protocol::Https || is_tg || is_ton)) { return Status::Error("Only HTTPS links are allowed"); } if (is_tg || is_ton) { if (http_only) { return Status::Error("Only HTTP links are allowed"); } if (tolower_begins_with(link, "http://") || http_url.protocol_ == HttpUrl::Protocol::Https || !http_url.userinfo_.empty() || http_url.specified_port_ != 0 || http_url.is_ipv6_) { return Status::Error(is_tg ? Slice("Wrong tg URL") : Slice("Wrong ton URL")); } Slice query(http_url.query_); CHECK(query[0] == '/'); if (query.size() > 1 && query[1] == '?') { query.remove_prefix(1); } for (auto c : http_url.host_) { if (!is_alnum(c) && c != '-' && c != '_') { return Status::Error("Unallowed characters in URL host"); } } return PSTRING() << (is_tg ? "tg" : "ton") << "://" << http_url.host_ << query; } if (http_url.host_.find('.') == string::npos && !http_url.is_ipv6_) { return Status::Error("Wrong HTTP URL"); } return http_url.get_url(); } LinkManager::LinkInfo LinkManager::get_link_info(Slice link) { LinkInfo result; if (link.empty()) { return result; } link.truncate(link.find('#')); bool is_tg = false; if (tolower_begins_with(link, "tg:")) { link.remove_prefix(3); if (begins_with(link, "//")) { link.remove_prefix(2); } is_tg = true; } auto r_http_url = parse_url(link); if (r_http_url.is_error()) { return result; } auto http_url = r_http_url.move_as_ok(); if (!http_url.userinfo_.empty() || http_url.is_ipv6_) { return result; } if (is_tg) { if (tolower_begins_with(link, "http://") || http_url.protocol_ == HttpUrl::Protocol::Https || http_url.specified_port_ != 0) { return result; } result.type_ = LinkType::Tg; result.query_ = link.str(); return result; } else { if (http_url.port_ != 80 && http_url.port_ != 443) { return result; } auto host = url_decode(http_url.host_, false); to_lower_inplace(host); if (ends_with(host, ".t.me") && host.size() >= 9 && host.find('.') == host.size() - 5) { Slice subdomain(&host[0], host.size() - 5); if (is_valid_username(subdomain) && subdomain != "addemoji" && subdomain != "addstickers" && subdomain != "addtheme" && subdomain != "auth" && subdomain != "confirmphone" && subdomain != "invoice" && subdomain != "joinchat" && subdomain != "login" && subdomain != "proxy" && subdomain != "setlanguage" && subdomain != "share" && subdomain != "socks" && subdomain != "web" && subdomain != "k" && subdomain != "z") { result.type_ = LinkType::TMe; result.query_ = PSTRING() << '/' << subdomain << http_url.query_; return result; } } if (begins_with(host, "www.")) { host = host.substr(4); } string cur_t_me_url; vector t_me_urls{Slice("t.me"), Slice("telegram.me"), Slice("telegram.dog")}; #if TD_EMSCRIPTEN t_me_urls.push_back(Slice("web.t.me")); t_me_urls.push_back(Slice("k.t.me")); t_me_urls.push_back(Slice("z.t.me")); #endif if (Scheduler::context() != nullptr) { // for tests only cur_t_me_url = G()->get_option_string("t_me_url"); if (tolower_begins_with(cur_t_me_url, "http://") || tolower_begins_with(cur_t_me_url, "https://")) { Slice t_me_url = cur_t_me_url; t_me_url = t_me_url.substr(t_me_url[4] == 's' ? 8 : 7); if (!td::contains(t_me_urls, t_me_url)) { t_me_urls.push_back(t_me_url); } } } for (auto t_me_url : t_me_urls) { if (host == t_me_url) { result.type_ = LinkType::TMe; Slice query = http_url.query_; while (true) { if (begins_with(query, "/s/")) { query.remove_prefix(2); continue; } if (begins_with(query, "/%73/")) { query.remove_prefix(4); continue; } break; } result.query_ = query.str(); return result; } } if (http_url.query_.size() > 1) { for (auto telegraph_url : {Slice("telegra.ph"), Slice("te.legra.ph"), Slice("graph.org")}) { if (host == telegraph_url) { result.type_ = LinkType::Telegraph; result.query_ = std::move(http_url.query_); return result; } } } } return result; } bool LinkManager::is_internal_link(Slice link) { auto info = get_link_info(link); return info.type_ != LinkType::External; } unique_ptr LinkManager::parse_internal_link(Slice link, bool is_trusted) { auto info = get_link_info(link); switch (info.type_) { case LinkType::External: return nullptr; case LinkType::Tg: return parse_tg_link_query(info.query_, is_trusted); case LinkType::TMe: return parse_t_me_link_query(info.query_, is_trusted); case LinkType::Telegraph: return td::make_unique(PSTRING() << "https://telegra.ph" << info.query_, link.str()); default: UNREACHABLE(); return nullptr; } } namespace { struct CopyArg { Slice name_; const HttpUrlQuery *url_query_; bool *is_first_; CopyArg(Slice name, const HttpUrlQuery *url_query, bool *is_first) : name_(name), url_query_(url_query), is_first_(is_first) { } }; StringBuilder &operator<<(StringBuilder &string_builder, const CopyArg ©_arg) { auto arg = copy_arg.url_query_->get_arg(copy_arg.name_); if (arg.empty()) { for (const auto &query_arg : copy_arg.url_query_->args_) { if (query_arg.first == copy_arg.name_) { char c = *copy_arg.is_first_ ? '?' : '&'; *copy_arg.is_first_ = false; return string_builder << c << copy_arg.name_; } } return string_builder; } char c = *copy_arg.is_first_ ? '?' : '&'; *copy_arg.is_first_ = false; return string_builder << c << copy_arg.name_ << '=' << url_encode(arg); } } // namespace unique_ptr LinkManager::parse_tg_link_query(Slice query, bool is_trusted) { const auto url_query = parse_url_query(query); const auto &path = url_query.path_; bool is_first_arg = true; auto copy_arg = [&](Slice name) { return CopyArg(name, &url_query, &is_first_arg); }; auto pass_arg = [&](Slice name) { return url_encode(url_query.get_arg(name)); }; auto get_arg = [&](Slice name) { return url_query.get_arg(name).str(); }; auto has_arg = [&](Slice name) { return !url_query.get_arg(name).empty(); }; if (path.size() == 1 && path[0] == "resolve") { 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")); } for (auto &arg : url_query.args_) { if (arg.first == "voicechat" || arg.first == "videochat" || arg.first == "livestream") { // resolve?domain=&videochat // resolve?domain=&videochat= if (Scheduler::context() != nullptr) { send_closure(G()->messages_manager(), &MessagesManager::reload_voice_chat_on_search, username); } return td::make_unique(std::move(username), arg.second, arg.first == "livestream"); } if (arg.first == "start" && is_valid_start_parameter(arg.second)) { // resolve?domain=&start= return td::make_unique(std::move(username), arg.second, is_trusted); } if (arg.first == "startgroup" && is_valid_start_parameter(arg.second)) { // resolve?domain=&startgroup= // resolve?domain=&startgroup=>parameter>&admin=change_info+delete_messages+restrict_members // resolve?domain=&startgroup&admin=change_info+delete_messages+restrict_members auto administrator_rights = get_administrator_rights(url_query.get_arg("admin"), false); return td::make_unique(std::move(username), arg.second, std::move(administrator_rights)); } if (arg.first == "startchannel") { // resolve?domain=&startchannel&admin=change_info+post_messages+promote_members auto administrator_rights = get_administrator_rights(url_query.get_arg("admin"), true); if (administrator_rights != AdministratorRights()) { return td::make_unique(std::move(username), std::move(administrator_rights)); } } if (arg.first == "game" && is_valid_game_name(arg.second)) { // resolve?domain=&game= 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()); } } if (!url_query.get_arg("attach").empty()) { // resolve?domain=&attach= // resolve?domain=&attach=&startattach= return td::make_unique( nullptr, td::make_unique(std::move(username)), url_query.get_arg("attach").str(), url_query.get_arg("startattach")); } else if (url_query.has_arg("startattach")) { // resolve?domain=&startattach&choose=users+bots+groups+channels // resolve?domain=&startattach=&choose=users+bots+groups+channels return td::make_unique(get_target_chat_chosen(url_query.get_arg("choose")), nullptr, std::move(username), url_query.get_arg("startattach")); } if (username == "telegrampassport") { // resolve?domain=telegrampassport&bot_id=...&scope=...&public_key=...&nonce=...&callback_url=... auto passport_link = get_internal_link_passport(query, url_query.args_, false); if (passport_link != nullptr) { return std::move(passport_link); } } // resolve?domain= return td::make_unique(std::move(username)); } else if (is_valid_phone_number(get_arg("phone"))) { auto user_link = td::make_unique(get_arg("phone")); if (!url_query.get_arg("attach").empty()) { // resolve?phone=&attach= // resolve?phone=&attach=&startattach= return td::make_unique( nullptr, std::move(user_link), url_query.get_arg("attach").str(), url_query.get_arg("startattach")); } // resolve?phone=12345 return std::move(user_link); } } else if (path.size() == 1 && path[0] == "contact") { // contact?token= if (has_arg("token")) { return td::make_unique(get_arg("token")); } } else if (path.size() == 1 && path[0] == "login") { // login?code=123456 if (has_arg("code")) { return td::make_unique(get_arg("code")); } // login?token= if (has_arg("token")) { return td::make_unique(); } } else if (path.size() == 1 && path[0] == "restore_purchases") { // restore_purchases return td::make_unique(); } else if (path.size() == 1 && path[0] == "passport") { // passport?bot_id=...&scope=...&public_key=...&nonce=...&callback_url=... return get_internal_link_passport(query, url_query.args_, true); } else if (path.size() == 1 && path[0] == "premium_offer") { // premium_offer?ref= return td::make_unique(get_arg("ref")); } else if (!path.empty() && path[0] == "settings") { if (path.size() == 2 && path[1] == "auto_delete") { // settings/auto_delete return td::make_unique(); } if (path.size() == 2 && path[1] == "change_number") { // settings/change_number return td::make_unique(); } if (path.size() == 2 && path[1] == "devices") { // settings/devices return td::make_unique(); } if (path.size() == 2 && path[1] == "edit_profile") { // settings/edit_profile return td::make_unique(); } if (path.size() == 2 && path[1] == "folders") { // settings/folders return td::make_unique(); } if (path.size() == 2 && path[1] == "language") { // settings/language return td::make_unique(); } if (path.size() == 2 && path[1] == "privacy") { // settings/privacy return td::make_unique(); } if (path.size() == 2 && path[1] == "themes") { // settings/themes return td::make_unique(); } // settings return td::make_unique(); } else if (path.size() == 1 && path[0] == "join") { // 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) && is_base64url_characters(invite_hash)) { return td::make_unique(PSTRING() << "tg:join?invite=" << url_encode(invite_hash)); } } } else if (path.size() == 1 && (path[0] == "addstickers" || path[0] == "addemoji")) { // addstickers?set= // addemoji?set= if (has_arg("set")) { return td::make_unique(get_arg("set"), path[0] == "addemoji"); } } else if (path.size() == 1 && path[0] == "setlanguage") { // setlanguage?lang= if (has_arg("lang")) { return td::make_unique(get_arg("lang")); } } else if (path.size() == 1 && path[0] == "addtheme") { // addtheme?slug= if (has_arg("slug")) { return td::make_unique(get_arg("slug")); } } else if (path.size() == 1 && path[0] == "confirmphone") { if (has_arg("hash") && has_arg("phone")) { // confirmphone?phone=&hash= return td::make_unique(get_arg("hash"), get_arg("phone")); } } else if (path.size() == 1 && path[0] == "socks") { if (has_arg("server") && has_arg("port")) { // socks?server=&port=&user=&pass= auto port = to_integer(get_arg("port")); if (0 < port && port < 65536) { return td::make_unique( get_arg("server"), port, td_api::make_object(get_arg("user"), get_arg("pass"))); } else { return td::make_unique(); } } } else if (path.size() == 1 && path[0] == "proxy") { if (has_arg("server") && has_arg("port")) { // proxy?server=&port=&secret= auto port = to_integer(get_arg("port")); auto r_secret = mtproto::ProxySecret::from_link(get_arg("secret")); if (0 < port && port < 65536 && r_secret.is_ok()) { return td::make_unique( get_arg("server"), port, td_api::make_object(r_secret.ok().get_encoded_secret())); } else { return td::make_unique(); } } } else if (path.size() == 1 && path[0] == "privatepost") { // privatepost?channel=123456789&post=12345&single&thread=&comment=&t= if (has_arg("channel") && has_arg("post")) { return td::make_unique( PSTRING() << "tg://privatepost" << copy_arg("channel") << copy_arg("post") << copy_arg("single") << copy_arg("thread") << copy_arg("comment") << copy_arg("t")); } } else if (path.size() == 1 && path[0] == "bg") { // bg?color= // bg?gradient=-&rotation=... // bg?gradient=~~~ // bg?slug=&mode=blur+motion // bg?slug=&intensity=...&bg_color=...&mode=blur+motion if (has_arg("color")) { return td::make_unique(pass_arg("color")); } if (has_arg("gradient")) { return td::make_unique(PSTRING() << pass_arg("gradient") << copy_arg("rotation")); } if (has_arg("slug")) { return td::make_unique(PSTRING() << pass_arg("slug") << copy_arg("mode") << copy_arg("intensity") << copy_arg("bg_color") << copy_arg("rotation")); } } else if (path.size() == 1 && path[0] == "invoice") { // invoice?slug= if (has_arg("slug")) { return td::make_unique(url_query.get_arg("slug").str()); } } else if (path.size() == 1 && (path[0] == "share" || path[0] == "msg" || path[0] == "msg_url")) { // msg_url?url= // msg_url?url=&text= return get_internal_link_message_draft(get_arg("url"), get_arg("text")); } if (!path.empty() && !path[0].empty()) { return td::make_unique(PSTRING() << "tg://" << query); } return nullptr; } unique_ptr LinkManager::parse_t_me_link_query(Slice query, bool is_trusted) { CHECK(query[0] == '/'); const auto url_query = parse_url_query(query); const auto &path = url_query.path_; if (path.empty() || path[0].empty()) { return nullptr; } bool is_first_arg = true; auto copy_arg = [&](Slice name) { return CopyArg(name, &url_query, &is_first_arg); }; auto get_arg = [&](Slice name) { return url_query.get_arg(name).str(); }; auto has_arg = [&](Slice name) { return !url_query.get_arg(name).empty(); }; if (path[0] == "c") { if (path.size() >= 3 && to_integer(path[1]) > 0 && to_integer(path[2]) > 0) { // /c/123456789/12345?single&thread=&comment=&t= // /c/123456789/1234/12345?single&comment=&t= is_first_arg = false; auto post = to_integer(path[2]); auto thread = PSTRING() << copy_arg("thread"); if (path.size() >= 4 && to_integer(path[3]) > 0) { thread = PSTRING() << "&thread=" << post; post = to_integer(path[3]); } return td::make_unique(PSTRING() << "tg://privatepost?channel=" << to_integer(path[1]) << "&post=" << post << copy_arg("single") << thread << copy_arg("comment") << copy_arg("t")); } } else if (path[0] == "login") { if (path.size() >= 2 && !path[1].empty()) { // /login/ return td::make_unique(path[1]); } } 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) && is_base64url_characters(invite_hash)) { // /joinchat/ return td::make_unique(PSTRING() << "tg:join?invite=" << url_encode(invite_hash)); } } } else if (path[0][0] == ' ' || path[0][0] == '+') { if (path[0].size() >= 2) { auto invite_hash = get_url_query_hash(false, url_query); if (is_valid_phone_number(invite_hash)) { auto user_link = td::make_unique(invite_hash); if (!url_query.get_arg("attach").empty()) { // /+?attach= // /+?attach=&startattach= return td::make_unique( nullptr, std::move(user_link), url_query.get_arg("attach").str(), url_query.get_arg("startattach")); } // /+ return std::move(user_link); } else if (!invite_hash.empty() && is_base64url_characters(invite_hash)) { // /+ return td::make_unique(PSTRING() << "tg:join?invite=" << url_encode(invite_hash)); } } } else if (path[0] == "contact") { if (path.size() >= 2 && !path[1].empty()) { // /contact/ return td::make_unique(path[1]); } } else if (path[0] == "addstickers" || path[0] == "addemoji") { if (path.size() >= 2 && !path[1].empty()) { // /addstickers/ // /addemoji/ return td::make_unique(path[1], path[0] == "addemoji"); } } else if (path[0] == "setlanguage") { if (path.size() >= 2 && !path[1].empty()) { // /setlanguage/ return td::make_unique(path[1]); } } else if (path[0] == "addtheme") { if (path.size() >= 2 && !path[1].empty()) { // /addtheme/ return td::make_unique(path[1]); } } else if (path[0] == "confirmphone") { if (has_arg("hash") && has_arg("phone")) { // /confirmphone?phone=&hash= return td::make_unique(get_arg("hash"), get_arg("phone")); } } else if (path[0] == "socks") { if (has_arg("server") && has_arg("port")) { // /socks?server=&port=&user=&pass= auto port = to_integer(get_arg("port")); if (0 < port && port < 65536) { return td::make_unique( get_arg("server"), port, td_api::make_object(get_arg("user"), get_arg("pass"))); } else { return td::make_unique(); } } } else if (path[0] == "proxy") { if (has_arg("server") && has_arg("port")) { // /proxy?server=&port=&secret= auto port = to_integer(get_arg("port")); auto r_secret = mtproto::ProxySecret::from_link(get_arg("secret")); if (0 < port && port < 65536 && r_secret.is_ok()) { return td::make_unique( get_arg("server"), port, td_api::make_object(r_secret.ok().get_encoded_secret())); } else { return td::make_unique(); } } } else if (path[0] == "bg") { if (path.size() >= 2 && !path[1].empty()) { // /bg/ // /bg/-?rotation=... // /bg/~~~ // /bg/?mode=blur+motion // /bg/?intensity=...&bg_color=...&mode=blur+motion if (BackgroundType::is_background_name_local(path[1])) { return td::make_unique(PSTRING() << url_encode(path[1]) << copy_arg("rotation")); } return td::make_unique(PSTRING() << url_encode(path[1]) << copy_arg("mode") << copy_arg("intensity") << copy_arg("bg_color") << copy_arg("rotation")); } } else if (path[0] == "invoice") { if (path.size() >= 2 && !path[1].empty()) { // /invoice/ return td::make_unique(path[1]); } } else if (path[0][0] == '$') { if (path[0].size() >= 2) { // /$ return td::make_unique(path[0].substr(1)); } } else if (path[0] == "share" || path[0] == "msg") { if (!(path.size() > 1 && (path[1] == "bookmarklet" || path[1] == "embed"))) { // /share?url= // /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() << 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) { // //12345?single&thread=&comment=&t= // //1234/12345?single&comment=&t= is_first_arg = false; auto post = to_integer(path[1]); auto thread = PSTRING() << copy_arg("thread"); if (path.size() >= 3 && to_integer(path[2]) > 0) { thread = PSTRING() << "&thread=" << post; post = to_integer(path[2]); } return td::make_unique(PSTRING() << "tg://resolve?domain=" << url_encode(path[0]) << "&post=" << post << copy_arg("single") << thread << 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]; for (auto &arg : url_query.args_) { if (arg.first == "voicechat" || arg.first == "videochat" || arg.first == "livestream") { // /?videochat // /?videochat= if (Scheduler::context() != nullptr) { send_closure(G()->messages_manager(), &MessagesManager::reload_voice_chat_on_search, username); } return td::make_unique(std::move(username), arg.second, arg.first == "livestream"); } if (arg.first == "start" && is_valid_start_parameter(arg.second)) { // /?start= return td::make_unique(std::move(username), arg.second, is_trusted); } if (arg.first == "startgroup" && is_valid_start_parameter(arg.second)) { // /?startgroup= // /?startgroup=&admin=change_info+delete_messages+restrict_members // /?startgroup&admin=change_info+delete_messages+restrict_members auto administrator_rights = get_administrator_rights(url_query.get_arg("admin"), false); return td::make_unique(std::move(username), arg.second, std::move(administrator_rights)); } if (arg.first == "startchannel") { // /?startchannel&admin=change_info+post_messages+promote_members auto administrator_rights = get_administrator_rights(url_query.get_arg("admin"), true); if (administrator_rights != AdministratorRights()) { return td::make_unique(std::move(username), std::move(administrator_rights)); } } if (arg.first == "game" && is_valid_game_name(arg.second)) { // /?game= return td::make_unique(std::move(username), arg.second); } } if (!url_query.get_arg("attach").empty()) { // /?attach= // /?attach=&startattach= return td::make_unique( nullptr, td::make_unique(std::move(username)), url_query.get_arg("attach").str(), url_query.get_arg("startattach")); } else if (url_query.has_arg("startattach")) { // /?startattach&choose=users+bots+groups+channels // /?startattach=&choose=users+bots+groups+channels return td::make_unique(get_target_chat_chosen(url_query.get_arg("choose")), nullptr, std::move(username), url_query.get_arg("startattach")); } // / return td::make_unique(std::move(username)); } return nullptr; } unique_ptr LinkManager::get_internal_link_message_draft(Slice url, Slice text) { if (url.empty() && text.empty()) { return nullptr; } while (!text.empty() && text.back() == '\n') { text.remove_suffix(1); } url = trim(url); if (url.empty()) { url = text; text = Slice(); } FormattedText full_text; bool contains_url = false; if (!text.empty()) { contains_url = true; full_text.text = PSTRING() << url << '\n' << text; } else { full_text.text = url.str(); } if (fix_formatted_text(full_text.text, full_text.entities, false, false, false, true, true).is_error()) { return nullptr; } if (full_text.text[0] == '@') { full_text.text = ' ' + full_text.text; for (auto &entity : full_text.entities) { entity.offset++; } } return td::make_unique(std::move(full_text), contains_url); } unique_ptr LinkManager::get_internal_link_passport( Slice query, const vector> &args, bool allow_unknown) { auto get_arg = [&args](Slice key) { for (auto &arg : args) { if (arg.first == key) { return Slice(arg.second); } } return Slice(); }; UserId bot_user_id(to_integer(get_arg("bot_id"))); auto scope = get_arg("scope"); auto public_key = get_arg("public_key"); auto nonce = get_arg("nonce"); if (nonce.empty()) { nonce = get_arg("payload"); } auto callback_url = get_arg("callback_url"); if (!bot_user_id.is_valid() || scope.empty() || public_key.empty() || nonce.empty()) { if (!allow_unknown) { return nullptr; } return td::make_unique(PSTRING() << "tg://" << query); } return td::make_unique(bot_user_id, scope.str(), public_key.str(), nonce.str(), 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 = 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(link->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); } void LinkManager::update_autologin_domains(vector autologin_domains, vector url_auth_domains, vector whitelisted_domains) { if (autologin_domains_ != autologin_domains) { autologin_domains_ = std::move(autologin_domains); G()->td_db()->get_binlog_pmc()->set("autologin_domains", implode(autologin_domains_, '\xFF')); } if (url_auth_domains_ != url_auth_domains) { url_auth_domains_ = std::move(url_auth_domains); G()->td_db()->get_binlog_pmc()->set("url_auth_domains", implode(url_auth_domains_, '\xFF')); } if (whitelisted_domains_ != whitelisted_domains) { whitelisted_domains_ = std::move(whitelisted_domains); G()->td_db()->get_binlog_pmc()->set("whitelisted_domains", implode(whitelisted_domains_, '\xFF')); } } void LinkManager::get_deep_link_info(Slice link, Promise> &&promise) { Slice link_scheme("tg:"); if (begins_with(link, link_scheme)) { link.remove_prefix(link_scheme.size()); if (begins_with(link, "//")) { link.remove_prefix(2); } } size_t pos = 0; while (pos < link.size() && link[pos] != '/' && link[pos] != '?' && link[pos] != '#') { pos++; } link.truncate(pos); td_->create_handler(std::move(promise))->send(link); } void LinkManager::get_external_link_info(string &&link, Promise> &&promise) { auto default_result = td_api::make_object(link, false); if (G()->close_flag()) { return promise.set_value(std::move(default_result)); } auto r_url = parse_url(link); if (r_url.is_error()) { return promise.set_value(std::move(default_result)); } auto url = r_url.move_as_ok(); if (!url.userinfo_.empty() || url.is_ipv6_) { return promise.set_value(std::move(default_result)); } bool skip_confirmation = td::contains(whitelisted_domains_, url.host_); default_result->skip_confirmation_ = skip_confirmation; if (!td::contains(autologin_domains_, url.host_)) { if (td::contains(url_auth_domains_, url.host_)) { td_->create_handler(std::move(promise))->send(link, FullMessageId(), 0); return; } return promise.set_value(std::move(default_result)); } if (autologin_update_time_ < Time::now() - 10000) { auto query_promise = PromiseCreator::lambda([link = std::move(link), default_result = std::move(default_result), promise = std::move(promise)](Result &&result) mutable { if (result.is_error()) { return promise.set_value(std::move(default_result)); } send_closure(G()->link_manager(), &LinkManager::get_external_link_info, std::move(link), std::move(promise)); }); return send_closure(G()->config_manager(), &ConfigManager::reget_config, std::move(query_promise)); } if (autologin_token_.empty()) { return promise.set_value(std::move(default_result)); } url.protocol_ = HttpUrl::Protocol::Https; Slice path = url.query_; path.truncate(url.query_.find_first_of("?#")); Slice parameters_hash = Slice(url.query_).substr(path.size()); Slice parameters = parameters_hash; parameters.truncate(parameters.find('#')); Slice hash = parameters_hash.substr(parameters.size()); string added_parameter; if (parameters.empty()) { added_parameter = '?'; } else if (parameters.size() == 1) { CHECK(parameters == "?"); } else { added_parameter = '&'; } added_parameter += "autologin_token="; added_parameter += autologin_token_; url.query_ = PSTRING() << path << parameters << added_parameter << hash; promise.set_value(td_api::make_object(url.get_url(), skip_confirmation)); } void LinkManager::get_login_url_info(FullMessageId full_message_id, int64 button_id, Promise> &&promise) { TRY_RESULT_PROMISE(promise, url, td_->messages_manager_->get_login_button_url(full_message_id, button_id)); td_->create_handler(std::move(promise)) ->send(std::move(url), full_message_id, narrow_cast(button_id)); } void LinkManager::get_login_url(FullMessageId full_message_id, int64 button_id, bool allow_write_access, Promise> &&promise) { TRY_RESULT_PROMISE(promise, url, td_->messages_manager_->get_login_button_url(full_message_id, button_id)); td_->create_handler(std::move(promise)) ->send(std::move(url), full_message_id, narrow_cast(button_id), allow_write_access); } void LinkManager::get_link_login_url(const string &url, bool allow_write_access, Promise> &&promise) { td_->create_handler(std::move(promise))->send(url, FullMessageId(), 0, allow_write_access); } Result LinkManager::get_background_url(const string &name, td_api::object_ptr background_type) { TRY_RESULT(type, BackgroundType::get_background_type(background_type.get())); auto url = PSTRING() << get_t_me_url() << "bg/"; auto link = type.get_link(); if (type.has_file()) { url += name; if (!link.empty()) { url += '?'; url += link; } } else { url += link; } return url; } string LinkManager::get_dialog_invite_link_hash(Slice invite_link) { auto link_info = get_link_info(invite_link); if (link_info.type_ != LinkType::Tg && link_info.type_ != LinkType::TMe) { return string(); } const auto url_query = parse_url_query(link_info.query_); auto invite_hash = get_url_query_hash(link_info.type_ == LinkType::Tg, url_query); if (is_valid_phone_number(invite_hash)) { return string(); } if (!is_base64url_characters(invite_hash)) { return string(); } return invite_hash; } string LinkManager::get_dialog_invite_link(Slice hash, bool is_internal) { if (!is_base64url_characters(hash)) { return string(); } if (is_internal) { return PSTRING() << "tg:join?invite=" << hash; } else { return PSTRING() << get_t_me_url() << '+' << hash; } } string LinkManager::get_instant_view_link_url(Slice link) { auto link_info = get_link_info(link); if (link_info.type_ != LinkType::TMe) { return string(); } const auto url_query = parse_url_query(link_info.query_); const auto &path = url_query.path_; if (path.size() == 1 && path[0] == "iv") { return url_query.get_arg("url").str(); } return string(); } string LinkManager::get_instant_view_link_rhash(Slice link) { auto link_info = get_link_info(link); if (link_info.type_ != LinkType::TMe) { return string(); } const auto url_query = parse_url_query(link_info.query_); const auto &path = url_query.path_; if (path.size() == 1 && path[0] == "iv" && !url_query.get_arg("url").empty()) { return url_query.get_arg("rhash").str(); } return string(); } 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_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) { string url = is_internal ? "tg://" : get_t_me_url(); bool is_socks = false; switch (proxy.type()) { case Proxy::Type::Socks5: url += "socks"; is_socks = true; break; case Proxy::Type::HttpTcp: case Proxy::Type::HttpCaching: return Status::Error(400, "HTTP proxies have no public links"); case Proxy::Type::Mtproto: url += "proxy"; break; default: UNREACHABLE(); } url += "?server="; url += url_encode(proxy.server()); url += "&port="; url += to_string(proxy.port()); if (is_socks) { if (!proxy.user().empty() || !proxy.password().empty()) { url += "&user="; url += url_encode(proxy.user()); url += "&pass="; url += url_encode(proxy.password()); } } else { url += "&secret="; url += proxy.secret().get_encoded_secret(); } return std::move(url); } UserId LinkManager::get_link_user_id(Slice url) { string lower_cased_url = to_lower(url); url = lower_cased_url; Slice link_scheme("tg:"); if (!begins_with(url, link_scheme)) { return UserId(); } url.remove_prefix(link_scheme.size()); if (begins_with(url, "//")) { url.remove_prefix(2); } Slice host("user"); if (!begins_with(url, host) || (url.size() > host.size() && Slice("/?#").find(url[host.size()]) == Slice::npos)) { return UserId(); } url.remove_prefix(host.size()); if (begins_with(url, "/")) { url.remove_prefix(1); } if (!begins_with(url, "?")) { return UserId(); } url.remove_prefix(1); url.truncate(url.find('#')); for (auto parameter : full_split(url, '&')) { Slice key; Slice value; std::tie(key, value) = split(parameter, '='); if (key == Slice("id")) { auto r_user_id = to_integer_safe(value); if (r_user_id.is_error()) { return UserId(); } return UserId(r_user_id.ok()); } } return UserId(); } string LinkManager::get_t_me_url() { if (Scheduler::context() != nullptr) { return G()->get_option_string("t_me_url", "https://t.me/"); } else { return "https://t.me/"; } } Result LinkManager::get_link_custom_emoji_id(Slice url) { string lower_cased_url = to_lower(url); url = lower_cased_url; Slice link_scheme("tg:"); if (!begins_with(url, link_scheme)) { return Status::Error(400, "Custom emoji URL must have scheme tg"); } url.remove_prefix(link_scheme.size()); if (begins_with(url, "//")) { url.remove_prefix(2); } Slice host("emoji"); if (!begins_with(url, host) || (url.size() > host.size() && Slice("/?#").find(url[host.size()]) == Slice::npos)) { return Status::Error(400, PSLICE() << "Custom emoji URL must have host \"" << host << '"'); } url.remove_prefix(host.size()); if (begins_with(url, "/")) { url.remove_prefix(1); } if (!begins_with(url, "?")) { return Status::Error(400, "Custom emoji URL must have an emoji identifier"); } url.remove_prefix(1); url.truncate(url.find('#')); for (auto parameter : full_split(url, '&')) { Slice key; Slice value; std::tie(key, value) = split(parameter, '='); if (key == Slice("id")) { auto r_document_id = to_integer_safe(value); if (r_document_id.is_error() || r_document_id.ok() == 0) { return Status::Error(400, "Invalid custom emoji identifier specified"); } return CustomEmojiId(r_document_id.ok()); } } return Status::Error(400, "Custom emoji URL must have an emoji identifier"); } Result LinkManager::get_message_link_info(Slice url) { if (url.empty()) { return Status::Error("URL must be non-empty"); } auto link_info = get_link_info(url); if (link_info.type_ != LinkType::Tg && link_info.type_ != LinkType::TMe) { return Status::Error("Invalid message link URL"); } url = link_info.query_; Slice username; Slice channel_id_slice; Slice message_id_slice; Slice comment_message_id_slice = "0"; Slice top_thread_message_id_slice; Slice media_timestamp_slice; bool is_single = false; bool for_comment = false; if (link_info.type_ == LinkType::Tg) { // resolve?domain=username&post=12345&single&t=123&comment=12&thread=21 // privatepost?channel=123456789&post=12345&single&t=123&comment=12&thread=21 bool is_resolve = false; if (begins_with(url, "resolve")) { url = url.substr(7); is_resolve = true; } else if (begins_with(url, "privatepost")) { url = url.substr(11); } else { return Status::Error("Wrong message link URL"); } if (begins_with(url, "/")) { url = url.substr(1); } if (!begins_with(url, "?")) { return Status::Error("Wrong message link URL"); } url = url.substr(1); auto args = full_split(url, '&'); for (auto arg : args) { auto key_value = split(arg, '='); if (is_resolve) { if (key_value.first == "domain") { username = key_value.second; } } else { if (key_value.first == "channel") { channel_id_slice = key_value.second; } } if (key_value.first == "post") { message_id_slice = key_value.second; } if (key_value.first == "t") { media_timestamp_slice = key_value.second; } if (key_value.first == "single") { is_single = true; } if (key_value.first == "comment") { comment_message_id_slice = key_value.second; } if (key_value.first == "thread") { for_comment = true; top_thread_message_id_slice = key_value.second; } } } else { // /c/123456789/12345 // /c/123456789/1234/12345 // /username/12345?single CHECK(!url.empty() && url[0] == '/'); url.remove_prefix(1); auto username_end_pos = url.find('/'); if (username_end_pos == Slice::npos) { return Status::Error("Wrong message link URL"); } username = url.substr(0, username_end_pos); url = url.substr(username_end_pos + 1); if (username == "c") { username = Slice(); auto channel_id_end_pos = url.find('/'); if (channel_id_end_pos == Slice::npos) { return Status::Error("Wrong message link URL"); } channel_id_slice = url.substr(0, channel_id_end_pos); url = url.substr(channel_id_end_pos + 1); } auto query_pos = url.find('?'); message_id_slice = url.substr(0, query_pos); if (query_pos != Slice::npos) { auto args = full_split(url.substr(query_pos + 1), '&'); for (auto arg : args) { auto key_value = split(arg, '='); if (key_value.first == "t") { media_timestamp_slice = key_value.second; } if (key_value.first == "single") { is_single = true; } if (key_value.first == "comment") { comment_message_id_slice = key_value.second; } if (key_value.first == "thread") { for_comment = true; top_thread_message_id_slice = key_value.second; } } } auto slash_pos = message_id_slice.find('/'); if (slash_pos != Slice::npos) { top_thread_message_id_slice = message_id_slice.substr(0, slash_pos); message_id_slice.remove_prefix(slash_pos + 1); } } ChannelId channel_id; if (username.empty()) { auto r_channel_id = to_integer_safe(channel_id_slice); if (r_channel_id.is_error() || !ChannelId(r_channel_id.ok()).is_valid()) { return Status::Error("Wrong channel ID"); } channel_id = ChannelId(r_channel_id.ok()); } auto r_message_id = to_integer_safe(message_id_slice); if (r_message_id.is_error() || !ServerMessageId(r_message_id.ok()).is_valid()) { return Status::Error("Wrong message ID"); } int32 top_thread_message_id = 0; if (!top_thread_message_id_slice.empty()) { auto r_top_thread_message_id = to_integer_safe(top_thread_message_id_slice); if (r_top_thread_message_id.is_error()) { return Status::Error("Wrong message thread ID"); } top_thread_message_id = r_top_thread_message_id.ok(); if (!ServerMessageId(top_thread_message_id).is_valid()) { return Status::Error("Invalid message thread ID"); } } auto r_comment_message_id = to_integer_safe(comment_message_id_slice); if (r_comment_message_id.is_error() || !(r_comment_message_id.ok() == 0 || ServerMessageId(r_comment_message_id.ok()).is_valid())) { return Status::Error("Wrong comment message ID"); } bool is_media_timestamp_invalid = false; int32 media_timestamp = 0; const int32 MAX_MEDIA_TIMESTAMP = 10000000; if (!media_timestamp_slice.empty()) { int32 current_value = 0; for (size_t i = 0; i <= media_timestamp_slice.size(); i++) { auto c = i < media_timestamp_slice.size() ? media_timestamp_slice[i] : 's'; if ('0' <= c && c <= '9') { current_value = current_value * 10 + c - '0'; if (current_value > MAX_MEDIA_TIMESTAMP) { is_media_timestamp_invalid = true; break; } } else { auto mul = 0; switch (to_lower(c)) { case 'h': mul = 3600; break; case 'm': mul = 60; break; case 's': mul = 1; break; } if (mul == 0 || current_value > MAX_MEDIA_TIMESTAMP / mul || media_timestamp + current_value * mul > MAX_MEDIA_TIMESTAMP) { is_media_timestamp_invalid = true; break; } media_timestamp += current_value * mul; current_value = 0; } } } MessageLinkInfo info; info.username = username.str(); info.channel_id = channel_id; info.message_id = MessageId(ServerMessageId(r_message_id.ok())); info.comment_message_id = MessageId(ServerMessageId(r_comment_message_id.ok())); info.top_thread_message_id = MessageId(ServerMessageId(top_thread_message_id)); info.media_timestamp = is_media_timestamp_invalid ? 0 : media_timestamp; info.is_single = is_single; info.for_comment = for_comment; LOG(INFO) << "Have link to " << info.message_id << " in chat @" << info.username << "/" << channel_id.get(); return std::move(info); } } // namespace td