//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021
//
// 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/ChannelId.h"
#include "td/telegram/ConfigManager.h"
#include "td/telegram/ConfigShared.h"
#include "td/telegram/ContactsManager.h"
#include "td/telegram/DialogId.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/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"
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_username(Slice username) {
if (username.empty() || username.size() > 32) {
return false;
}
if (!is_alpha(username[0])) {
return false;
}
for (auto c : username) {
if (!is_alpha(c) && !is_digit(c) && c != '_') {
return false;
}
}
if (username.back() == '_') {
return false;
}
for (size_t i = 1; i < username.size(); i++) {
if (username[i - 1] == '_' && username[i] == '_') {
return false;
}
}
return true;
}
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.size() >= 1 && path[0].size() >= 2 && (path[0][0] == ' ' || path[0][0] == '+')) {
// /+
return path[0].substr(1);
}
}
return string();
}
class LinkManager::InternalLinkActiveSessions final : public InternalLink {
td_api::object_ptr get_internal_link_type_object() const final {
return td_api::make_object();
}
};
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::InternalLinkBotStart final : public InternalLink {
string bot_username_;
string start_parameter_;
td_api::object_ptr get_internal_link_type_object() const final {
return td_api::make_object(bot_username_, start_parameter_);
}
public:
InternalLinkBotStart(string bot_username, string start_parameter)
: bot_username_(std::move(bot_username)), start_parameter_(std::move(start_parameter)) {
}
};
class LinkManager::InternalLinkBotStartInGroup final : public InternalLink {
string bot_username_;
string start_parameter_;
td_api::object_ptr get_internal_link_type_object() const final {
return td_api::make_object(bot_username_, start_parameter_);
}
public:
InternalLinkBotStartInGroup(string bot_username, string start_parameter)
: bot_username_(std::move(bot_username)), start_parameter_(std::move(start_parameter)) {
}
};
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::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::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::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::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::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 proxy_type = [type = type_.get()]() -> 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::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_;
td_api::object_ptr get_internal_link_type_object() const final {
return td_api::make_object(sticker_set_name_);
}
public:
explicit InternalLinkStickerSet(string sticker_set_name) : sticker_set_name_(std::move(sticker_set_name)) {
}
};
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::InternalLinkVoiceChat final : public InternalLink {
string dialog_username_;
string invite_hash_;
td_api::object_ptr get_internal_link_type_object() const final {
return td_api::make_object(dialog_username_, invite_hash_);
}
public:
InternalLinkVoiceChat(string dialog_username, string invite_hash)
: dialog_username_(std::move(dialog_username)), invite_hash_(std::move(invite_hash)) {
}
};
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(uint64 id, BufferSlice packet) final {
auto result_ptr = fetch_result(packet);
if (result_ptr.is_error()) {
return on_error(id, 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);
bool need_update = (info->flags_ & telegram_api::help_deepLinkInfo::UPDATE_APP_MASK) != 0;
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), need_update));
}
default:
UNREACHABLE();
}
}
void on_error(uint64 id, 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(uint64 id, BufferSlice packet) final {
auto result_ptr = fetch_result(packet);
if (result_ptr.is_error()) {
return on_error(id, 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: {
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(id, Status::Error(500, "Receive invalid bot_user_id"));
}
td->contacts_manager_->on_get_user(std::move(request->bot_), "RequestUrlAuthQuery");
bool request_write_access =
(request->flags_ & telegram_api::urlAuthResultRequest::REQUEST_WRITE_ACCESS_MASK) != 0;
promise_.set_value(td_api::make_object(
url_, request->domain_, td->contacts_manager_->get_user_id_object(bot_user_id, "RequestUrlAuthQuery"),
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(uint64 id, Status status) final {
if (!dialog_id_.is_valid() ||
!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "RequestUrlAuthQuery")) {
LOG(INFO) << "RequestUrlAuthQuery returned " << 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(uint64 id, BufferSlice packet) final {
auto result_ptr = fetch_result(packet);
if (result_ptr.is_error()) {
return on_error(id, 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(id, 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(uint64 id, Status status) final {
if (!dialog_id_.is_valid() ||
!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "AcceptUrlAuthQuery")) {
LOG(INFO) << "AcceptUrlAuthQuery returned " << 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');
}
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(Slice link) {
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 (is_tg || is_ton) {
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.is_internal_ = true;
result.is_tg_ = true;
result.query_ = link.str();
return result;
} else {
if (http_url.port_ != 80 && http_url.port_ != 443) {
return result;
}
vector t_me_urls{Slice("t.me"), Slice("telegram.me"), Slice("telegram.dog")};
if (Scheduler::context() != nullptr) { // for tests only
string cur_t_me_url = G()->shared_config().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);
}
}
}
auto host = url_decode(http_url.host_, false);
to_lower_inplace(host);
if (begins_with(host, "www.")) {
host = host.substr(4);
}
for (auto t_me_url : t_me_urls) {
if (host == t_me_url) {
result.is_internal_ = true;
result.is_tg_ = false;
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;
}
}
}
return result;
}
unique_ptr LinkManager::parse_internal_link(Slice link) {
auto info = get_link_info(link);
if (!info.is_internal_) {
return nullptr;
}
if (info.is_tg_) {
return parse_tg_link_query(info.query_);
} else {
return parse_t_me_link_query(info.query_);
}
}
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) {
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") {
if (is_valid_username(get_arg("domain"))) {
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") {
// resolve?domain=&voicechat
// resolve?domain=&voicechat=
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);
}
if (arg.first == "start" && is_valid_start_parameter(arg.second)) {
// resolve?domain=?start=
return td::make_unique(std::move(username), arg.second);
}
if (arg.first == "startgroup" && is_valid_start_parameter(arg.second)) {
// resolve?domain=?startgroup=
return td::make_unique(std::move(username), arg.second);
}
if (arg.first == "game" && !arg.second.empty()) {
// resolve?domain=?game=
return td::make_unique(std::move(username), arg.second);
}
}
if (username == "telegrampassport") {
// resolve?domain=telegrampassport&bot_id=&scope=&public_key=&nonce=
return get_internal_link_passport(query, url_query.args_);
}
// resolve?domain=
return td::make_unique(std::move(username));
}
} 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] == "passport") {
// passport?bot_id=&scope=&public_key=&nonce=
return get_internal_link_passport(query, url_query.args_);
} else if (path.size() >= 1 && path[0] == "settings") {
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] == "folders") {
// settings/folders
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")) {
return td::make_unique(PSTRING() << "tg:join?invite="
<< url_encode(get_url_query_hash(true, url_query)));
}
} else if (path.size() == 1 && path[0] == "addstickers") {
// addstickers?set=
if (has_arg("set")) {
return td::make_unique(get_arg("set"));
}
} 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 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"));
if (0 < port && port < 65536 && mtproto::ProxySecret::from_link(get_arg("secret")).is_ok()) {
return td::make_unique(get_arg("server"), port,
td_api::make_object(get_arg("secret")));
}
}
} else if (path.size() == 1 && path[0] == "privatepost") {
// privatepost?channel=123456789&msg_id=12345&single&thread=&comment=&t=
if (has_arg("channel") && has_arg("msg_id")) {
return td::make_unique(
PSTRING() << "tg:privatepost" << copy_arg("channel") << copy_arg("msg_id") << 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] == "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) {
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=
is_first_arg = false;
return td::make_unique(PSTRING()
<< "tg:privatepost?channel=" << to_integer(path[1])
<< "&msg_id=" << to_integer(path[2]) << copy_arg("single")
<< copy_arg("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()) {
// /joinchat/
return td::make_unique(PSTRING() << "tg:join?invite="
<< url_encode(get_url_query_hash(false, url_query)));
}
} else if (path[0][0] == ' ' || path[0][0] == '+') {
if (path[0].size() >= 2) {
// /+
return td::make_unique(
PSTRING() << "tg:join?invite=" + url_encode(get_url_query_hash(false, url_query)));
}
} else if (path[0] == "addstickers") {
if (path.size() >= 2 && !path[1].empty()) {
// /addstickers/
return td::make_unique(path[1]);
}
} 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 if (path[0] == "proxy") {
if (has_arg("server") && has_arg("port")) {
// /proxy?server=&port=&secret=
auto port = to_integer(get_arg("port"));
if (0 < port && port < 65536 && mtproto::ProxySecret::from_link(get_arg("secret")).is_ok()) {
return td::make_unique(get_arg("server"), port,
td_api::make_object(get_arg("secret")));
}
}
} 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
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] == "share" || path[0] == "msg") {
if (!(path.size() > 1 && (path[1] == "bookmarklet" || path[1] == "embed"))) {
// /share?url=
// /share/url?url=&text=
return get_internal_link_message_draft(get_arg("url"), get_arg("text"));
}
} else if (is_valid_username(path[0])) {
if (path.size() >= 2 && to_integer(path[1]) > 0) {
// //12345?single&thread=&comment=&t=
is_first_arg = false;
return td::make_unique(
PSTRING() << "tg:resolve?domain=" << url_encode(path[0]) << "&post=" << to_integer(path[1])
<< copy_arg("single") << copy_arg("thread") << copy_arg("comment") << copy_arg("t"));
}
auto username = path[0];
for (auto &arg : url_query.args_) {
if (arg.first == "voicechat") {
// /?voicechat
// /?voicechat=
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);
}
if (arg.first == "start" && is_valid_start_parameter(arg.second)) {
// /?start=
return td::make_unique(std::move(username), arg.second);
}
if (arg.first == "startgroup" && is_valid_start_parameter(arg.second)) {
// /?startgroup=
return td::make_unique(std::move(username), arg.second);
}
if (arg.first == "game" && !arg.second.empty()) {
// /?game=
return td::make_unique(std::move(username), arg.second);
}
}
// /
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) {
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()) {
return td::make_unique(PSTRING() << "tg://" << query);
}
return td::make_unique(bot_user_id, scope.str(), public_key.str(), nonce.str(),
callback_url.str());
}
void LinkManager::update_autologin_domains(string autologin_token, vector autologin_domains,
vector url_auth_domains) {
autologin_update_time_ = Time::now();
autologin_token_ = std::move(autologin_token);
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'));
}
}
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));
}
if (!td::contains(autologin_domains_, r_url.ok().host_)) {
if (td::contains(url_auth_domains_, r_url.ok().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), promise = std::move(promise)](
Result> &&result) mutable {
if (result.is_error()) {
return promise.set_value(td_api::make_object(link, false));
}
send_closure(G()->link_manager(), &LinkManager::get_external_link_info, std::move(link), std::move(promise));
});
return send_closure(G()->config_manager(), &ConfigManager::get_app_config, std::move(query_promise));
}
if (autologin_token_.empty()) {
return promise.set_value(std::move(default_result));
}
auto url = r_url.move_as_ok();
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(), false));
}
void LinkManager::get_login_url_info(FullMessageId full_message_id, int32 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, button_id);
}
void LinkManager::get_login_url(FullMessageId full_message_id, int32 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, 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);
}
string LinkManager::get_dialog_invite_link_hash(Slice invite_link) {
auto link_info = get_link_info(invite_link);
if (!link_info.is_internal_) {
return string();
}
const auto url_query = parse_url_query(link_info.query_);
return get_url_query_hash(link_info.is_tg_, url_query);
}
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.is_internal_) {
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 media_timestamp_slice;
bool is_single = false;
bool for_comment = false;
if (link_info.is_tg_) {
// resolve?domain=username&post=12345&single&t=123&comment=12&thread=21
// privatepost?channel=123456789&msg_id=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;
}
if (key_value.first == "post") {
message_id_slice = key_value.second;
}
} else {
if (key_value.first == "channel") {
channel_id_slice = key_value.second;
}
if (key_value.first == "msg_id") {
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;
}
}
} else {
// /c/123456789/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;
}
}
}
}
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