1 //
2 // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021
3 //
4 // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 //
7 #include "td/telegram/LinkManager.h"
8
9 #include "td/telegram/AccessRights.h"
10 #include "td/telegram/ChannelId.h"
11 #include "td/telegram/ConfigManager.h"
12 #include "td/telegram/ConfigShared.h"
13 #include "td/telegram/ContactsManager.h"
14 #include "td/telegram/DialogId.h"
15 #include "td/telegram/Global.h"
16 #include "td/telegram/MessageEntity.h"
17 #include "td/telegram/MessageId.h"
18 #include "td/telegram/MessagesManager.h"
19 #include "td/telegram/misc.h"
20 #include "td/telegram/ServerMessageId.h"
21 #include "td/telegram/Td.h"
22 #include "td/telegram/TdDb.h"
23 #include "td/telegram/telegram_api.h"
24 #include "td/telegram/UserId.h"
25
26 #include "td/mtproto/ProxySecret.h"
27
28 #include "td/utils/algorithm.h"
29 #include "td/utils/base64.h"
30 #include "td/utils/buffer.h"
31 #include "td/utils/HttpUrl.h"
32 #include "td/utils/logging.h"
33 #include "td/utils/misc.h"
34 #include "td/utils/SliceBuilder.h"
35 #include "td/utils/StringBuilder.h"
36 #include "td/utils/Time.h"
37
38 #include <tuple>
39
40 namespace td {
41
is_valid_start_parameter(Slice start_parameter)42 static bool is_valid_start_parameter(Slice start_parameter) {
43 return start_parameter.size() <= 64 && is_base64url_characters(start_parameter);
44 }
45
is_valid_username(Slice username)46 static bool is_valid_username(Slice username) {
47 if (username.empty() || username.size() > 32) {
48 return false;
49 }
50 if (!is_alpha(username[0])) {
51 return false;
52 }
53 for (auto c : username) {
54 if (!is_alpha(c) && !is_digit(c) && c != '_') {
55 return false;
56 }
57 }
58 if (username.back() == '_') {
59 return false;
60 }
61 for (size_t i = 1; i < username.size(); i++) {
62 if (username[i - 1] == '_' && username[i] == '_') {
63 return false;
64 }
65 }
66
67 return true;
68 }
69
get_url_query_hash(bool is_tg,const HttpUrlQuery & url_query)70 static string get_url_query_hash(bool is_tg, const HttpUrlQuery &url_query) {
71 const auto &path = url_query.path_;
72 if (is_tg) {
73 if (path.size() == 1 && path[0] == "join" && !url_query.get_arg("invite").empty()) {
74 // join?invite=abcdef
75 return url_query.get_arg("invite").str();
76 }
77 } else {
78 if (path.size() >= 2 && path[0] == "joinchat" && !path[1].empty()) {
79 // /joinchat/<link>
80 return path[1];
81 }
82 if (!path.empty() && path[0].size() >= 2 && (path[0][0] == ' ' || path[0][0] == '+')) {
83 // /+<link>
84 return path[0].substr(1);
85 }
86 }
87 return string();
88 }
89
90 class LinkManager::InternalLinkActiveSessions final : public InternalLink {
get_internal_link_type_object() const91 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
92 return td_api::make_object<td_api::internalLinkTypeActiveSessions>();
93 }
94 };
95
96 class LinkManager::InternalLinkAuthenticationCode final : public InternalLink {
97 string code_;
98
get_internal_link_type_object() const99 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
100 return td_api::make_object<td_api::internalLinkTypeAuthenticationCode>(code_);
101 }
102
103 public:
InternalLinkAuthenticationCode(string code)104 explicit InternalLinkAuthenticationCode(string code) : code_(std::move(code)) {
105 }
106 };
107
108 class LinkManager::InternalLinkBackground final : public InternalLink {
109 string background_name_;
110
get_internal_link_type_object() const111 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
112 return td_api::make_object<td_api::internalLinkTypeBackground>(background_name_);
113 }
114
115 public:
InternalLinkBackground(string background_name)116 explicit InternalLinkBackground(string background_name) : background_name_(std::move(background_name)) {
117 }
118 };
119
120 class LinkManager::InternalLinkBotStart final : public InternalLink {
121 string bot_username_;
122 string start_parameter_;
123
get_internal_link_type_object() const124 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
125 return td_api::make_object<td_api::internalLinkTypeBotStart>(bot_username_, start_parameter_);
126 }
127
128 public:
InternalLinkBotStart(string bot_username,string start_parameter)129 InternalLinkBotStart(string bot_username, string start_parameter)
130 : bot_username_(std::move(bot_username)), start_parameter_(std::move(start_parameter)) {
131 }
132 };
133
134 class LinkManager::InternalLinkBotStartInGroup final : public InternalLink {
135 string bot_username_;
136 string start_parameter_;
137
get_internal_link_type_object() const138 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
139 return td_api::make_object<td_api::internalLinkTypeBotStartInGroup>(bot_username_, start_parameter_);
140 }
141
142 public:
InternalLinkBotStartInGroup(string bot_username,string start_parameter)143 InternalLinkBotStartInGroup(string bot_username, string start_parameter)
144 : bot_username_(std::move(bot_username)), start_parameter_(std::move(start_parameter)) {
145 }
146 };
147
148 class LinkManager::InternalLinkChangePhoneNumber final : public InternalLink {
get_internal_link_type_object() const149 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
150 return td_api::make_object<td_api::internalLinkTypeChangePhoneNumber>();
151 }
152 };
153
154 class LinkManager::InternalLinkConfirmPhone final : public InternalLink {
155 string hash_;
156 string phone_number_;
157
get_internal_link_type_object() const158 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
159 return td_api::make_object<td_api::internalLinkTypePhoneNumberConfirmation>(hash_, phone_number_);
160 }
161
162 public:
InternalLinkConfirmPhone(string hash,string phone_number)163 InternalLinkConfirmPhone(string hash, string phone_number)
164 : hash_(std::move(hash)), phone_number_(std::move(phone_number)) {
165 }
166 };
167
168 class LinkManager::InternalLinkDialogInvite final : public InternalLink {
169 string url_;
170
get_internal_link_type_object() const171 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
172 return td_api::make_object<td_api::internalLinkTypeChatInvite>(url_);
173 }
174
175 public:
InternalLinkDialogInvite(string url)176 explicit InternalLinkDialogInvite(string url) : url_(std::move(url)) {
177 }
178 };
179
180 class LinkManager::InternalLinkFilterSettings final : public InternalLink {
get_internal_link_type_object() const181 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
182 return td_api::make_object<td_api::internalLinkTypeFilterSettings>();
183 }
184 };
185
186 class LinkManager::InternalLinkGame final : public InternalLink {
187 string bot_username_;
188 string game_short_name_;
189
get_internal_link_type_object() const190 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
191 return td_api::make_object<td_api::internalLinkTypeGame>(bot_username_, game_short_name_);
192 }
193
194 public:
InternalLinkGame(string bot_username,string game_short_name)195 InternalLinkGame(string bot_username, string game_short_name)
196 : bot_username_(std::move(bot_username)), game_short_name_(std::move(game_short_name)) {
197 }
198 };
199
200 class LinkManager::InternalLinkLanguage final : public InternalLink {
201 string language_pack_id_;
202
get_internal_link_type_object() const203 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
204 return td_api::make_object<td_api::internalLinkTypeLanguagePack>(language_pack_id_);
205 }
206
207 public:
InternalLinkLanguage(string language_pack_id)208 explicit InternalLinkLanguage(string language_pack_id) : language_pack_id_(std::move(language_pack_id)) {
209 }
210 };
211
212 class LinkManager::InternalLinkMessage final : public InternalLink {
213 string url_;
214
get_internal_link_type_object() const215 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
216 return td_api::make_object<td_api::internalLinkTypeMessage>(url_);
217 }
218
219 public:
InternalLinkMessage(string url)220 explicit InternalLinkMessage(string url) : url_(std::move(url)) {
221 }
222 };
223
224 class LinkManager::InternalLinkMessageDraft final : public InternalLink {
225 FormattedText text_;
226 bool contains_link_ = false;
227
get_internal_link_type_object() const228 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
229 return td_api::make_object<td_api::internalLinkTypeMessageDraft>(get_formatted_text_object(text_, true, -1),
230 contains_link_);
231 }
232
233 public:
InternalLinkMessageDraft(FormattedText && text,bool contains_link)234 InternalLinkMessageDraft(FormattedText &&text, bool contains_link)
235 : text_(std::move(text)), contains_link_(contains_link) {
236 }
237 };
238
239 class LinkManager::InternalLinkPassportDataRequest final : public InternalLink {
240 UserId bot_user_id_;
241 string scope_;
242 string public_key_;
243 string nonce_;
244 string callback_url_;
245
get_internal_link_type_object() const246 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
247 return td_api::make_object<td_api::internalLinkTypePassportDataRequest>(bot_user_id_.get(), scope_, public_key_,
248 nonce_, callback_url_);
249 }
250
251 public:
InternalLinkPassportDataRequest(UserId bot_user_id,string scope,string public_key,string nonce,string callback_url)252 InternalLinkPassportDataRequest(UserId bot_user_id, string scope, string public_key, string nonce,
253 string callback_url)
254 : bot_user_id_(bot_user_id)
255 , scope_(std::move(scope))
256 , public_key_(std::move(public_key))
257 , nonce_(std::move(nonce))
258 , callback_url_(std::move(callback_url)) {
259 }
260 };
261
262 class LinkManager::InternalLinkProxy final : public InternalLink {
263 string server_;
264 int32 port_;
265 td_api::object_ptr<td_api::ProxyType> type_;
266
get_internal_link_type_object() const267 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
268 CHECK(type_ != nullptr);
269 auto proxy_type = [type = type_.get()]() -> td_api::object_ptr<td_api::ProxyType> {
270 switch (type->get_id()) {
271 case td_api::proxyTypeSocks5::ID: {
272 auto type_socks = static_cast<const td_api::proxyTypeSocks5 *>(type);
273 return td_api::make_object<td_api::proxyTypeSocks5>(type_socks->username_, type_socks->password_);
274 }
275 case td_api::proxyTypeMtproto::ID: {
276 auto type_mtproto = static_cast<const td_api::proxyTypeMtproto *>(type);
277 return td_api::make_object<td_api::proxyTypeMtproto>(type_mtproto->secret_);
278 }
279 default:
280 UNREACHABLE();
281 return nullptr;
282 }
283 }();
284 return td_api::make_object<td_api::internalLinkTypeProxy>(server_, port_, std::move(proxy_type));
285 }
286
287 public:
InternalLinkProxy(string server,int32 port,td_api::object_ptr<td_api::ProxyType> type)288 InternalLinkProxy(string server, int32 port, td_api::object_ptr<td_api::ProxyType> type)
289 : server_(std::move(server)), port_(port), type_(std::move(type)) {
290 }
291 };
292
293 class LinkManager::InternalLinkPublicDialog final : public InternalLink {
294 string dialog_username_;
295
get_internal_link_type_object() const296 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
297 return td_api::make_object<td_api::internalLinkTypePublicChat>(dialog_username_);
298 }
299
300 public:
InternalLinkPublicDialog(string dialog_username)301 explicit InternalLinkPublicDialog(string dialog_username) : dialog_username_(std::move(dialog_username)) {
302 }
303 };
304
305 class LinkManager::InternalLinkQrCodeAuthentication final : public InternalLink {
get_internal_link_type_object() const306 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
307 return td_api::make_object<td_api::internalLinkTypeQrCodeAuthentication>();
308 }
309 };
310
311 class LinkManager::InternalLinkSettings final : public InternalLink {
get_internal_link_type_object() const312 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
313 return td_api::make_object<td_api::internalLinkTypeSettings>();
314 }
315 };
316
317 class LinkManager::InternalLinkStickerSet final : public InternalLink {
318 string sticker_set_name_;
319
get_internal_link_type_object() const320 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
321 return td_api::make_object<td_api::internalLinkTypeStickerSet>(sticker_set_name_);
322 }
323
324 public:
InternalLinkStickerSet(string sticker_set_name)325 explicit InternalLinkStickerSet(string sticker_set_name) : sticker_set_name_(std::move(sticker_set_name)) {
326 }
327 };
328
329 class LinkManager::InternalLinkTheme final : public InternalLink {
330 string theme_name_;
331
get_internal_link_type_object() const332 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
333 return td_api::make_object<td_api::internalLinkTypeTheme>(theme_name_);
334 }
335
336 public:
InternalLinkTheme(string theme_name)337 explicit InternalLinkTheme(string theme_name) : theme_name_(std::move(theme_name)) {
338 }
339 };
340
341 class LinkManager::InternalLinkThemeSettings final : public InternalLink {
get_internal_link_type_object() const342 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
343 return td_api::make_object<td_api::internalLinkTypeThemeSettings>();
344 }
345 };
346
347 class LinkManager::InternalLinkUnknownDeepLink final : public InternalLink {
348 string link_;
349
get_internal_link_type_object() const350 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
351 return td_api::make_object<td_api::internalLinkTypeUnknownDeepLink>(link_);
352 }
353
354 public:
InternalLinkUnknownDeepLink(string link)355 explicit InternalLinkUnknownDeepLink(string link) : link_(std::move(link)) {
356 }
357 };
358
359 class LinkManager::InternalLinkUnsupportedProxy final : public InternalLink {
get_internal_link_type_object() const360 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
361 return td_api::make_object<td_api::internalLinkTypeUnsupportedProxy>();
362 }
363 };
364
365 class LinkManager::InternalLinkVoiceChat final : public InternalLink {
366 string dialog_username_;
367 string invite_hash_;
368 bool is_live_stream_;
369
get_internal_link_type_object() const370 td_api::object_ptr<td_api::InternalLinkType> get_internal_link_type_object() const final {
371 return td_api::make_object<td_api::internalLinkTypeVideoChat>(dialog_username_, invite_hash_, is_live_stream_);
372 }
373
374 public:
InternalLinkVoiceChat(string dialog_username,string invite_hash,bool is_live_stream)375 InternalLinkVoiceChat(string dialog_username, string invite_hash, bool is_live_stream)
376 : dialog_username_(std::move(dialog_username))
377 , invite_hash_(std::move(invite_hash))
378 , is_live_stream_(is_live_stream) {
379 }
380 };
381
382 class GetDeepLinkInfoQuery final : public Td::ResultHandler {
383 Promise<td_api::object_ptr<td_api::deepLinkInfo>> promise_;
384
385 public:
GetDeepLinkInfoQuery(Promise<td_api::object_ptr<td_api::deepLinkInfo>> && promise)386 explicit GetDeepLinkInfoQuery(Promise<td_api::object_ptr<td_api::deepLinkInfo>> &&promise)
387 : promise_(std::move(promise)) {
388 }
389
send(Slice link)390 void send(Slice link) {
391 send_query(G()->net_query_creator().create_unauth(telegram_api::help_getDeepLinkInfo(link.str())));
392 }
393
on_result(BufferSlice packet)394 void on_result(BufferSlice packet) final {
395 auto result_ptr = fetch_result<telegram_api::help_getDeepLinkInfo>(packet);
396 if (result_ptr.is_error()) {
397 return on_error(result_ptr.move_as_error());
398 }
399
400 auto result = result_ptr.move_as_ok();
401 switch (result->get_id()) {
402 case telegram_api::help_deepLinkInfoEmpty::ID:
403 return promise_.set_value(nullptr);
404 case telegram_api::help_deepLinkInfo::ID: {
405 auto info = telegram_api::move_object_as<telegram_api::help_deepLinkInfo>(result);
406 auto entities = get_message_entities(nullptr, std::move(info->entities_), "GetDeepLinkInfoQuery");
407 auto status = fix_formatted_text(info->message_, entities, true, true, true, true, true);
408 if (status.is_error()) {
409 LOG(ERROR) << "Receive error " << status << " while parsing deep link info " << info->message_;
410 if (!clean_input_string(info->message_)) {
411 info->message_.clear();
412 }
413 entities = find_entities(info->message_, true, true);
414 }
415 FormattedText text{std::move(info->message_), std::move(entities)};
416 return promise_.set_value(
417 td_api::make_object<td_api::deepLinkInfo>(get_formatted_text_object(text, true, -1), info->update_app_));
418 }
419 default:
420 UNREACHABLE();
421 }
422 }
423
on_error(Status status)424 void on_error(Status status) final {
425 promise_.set_error(std::move(status));
426 }
427 };
428
429 class RequestUrlAuthQuery final : public Td::ResultHandler {
430 Promise<td_api::object_ptr<td_api::LoginUrlInfo>> promise_;
431 string url_;
432 DialogId dialog_id_;
433
434 public:
RequestUrlAuthQuery(Promise<td_api::object_ptr<td_api::LoginUrlInfo>> && promise)435 explicit RequestUrlAuthQuery(Promise<td_api::object_ptr<td_api::LoginUrlInfo>> &&promise)
436 : promise_(std::move(promise)) {
437 }
438
send(string url,FullMessageId full_message_id,int32 button_id)439 void send(string url, FullMessageId full_message_id, int32 button_id) {
440 url_ = std::move(url);
441 int32 flags = 0;
442 tl_object_ptr<telegram_api::InputPeer> input_peer;
443 if (full_message_id.get_dialog_id().is_valid()) {
444 dialog_id_ = full_message_id.get_dialog_id();
445 input_peer = td_->messages_manager_->get_input_peer(dialog_id_, AccessRights::Read);
446 CHECK(input_peer != nullptr);
447 flags |= telegram_api::messages_requestUrlAuth::PEER_MASK;
448 } else {
449 flags |= telegram_api::messages_requestUrlAuth::URL_MASK;
450 }
451 send_query(G()->net_query_creator().create(telegram_api::messages_requestUrlAuth(
452 flags, std::move(input_peer), full_message_id.get_message_id().get_server_message_id().get(), button_id,
453 url_)));
454 }
455
on_result(BufferSlice packet)456 void on_result(BufferSlice packet) final {
457 auto result_ptr = fetch_result<telegram_api::messages_requestUrlAuth>(packet);
458 if (result_ptr.is_error()) {
459 return on_error(result_ptr.move_as_error());
460 }
461
462 auto result = result_ptr.move_as_ok();
463 LOG(INFO) << "Receive result for RequestUrlAuthQuery: " << to_string(result);
464 switch (result->get_id()) {
465 case telegram_api::urlAuthResultRequest::ID: {
466 auto request = telegram_api::move_object_as<telegram_api::urlAuthResultRequest>(result);
467 UserId bot_user_id = ContactsManager::get_user_id(request->bot_);
468 if (!bot_user_id.is_valid()) {
469 return on_error(Status::Error(500, "Receive invalid bot_user_id"));
470 }
471 td_->contacts_manager_->on_get_user(std::move(request->bot_), "RequestUrlAuthQuery");
472 promise_.set_value(td_api::make_object<td_api::loginUrlInfoRequestConfirmation>(
473 url_, request->domain_, td_->contacts_manager_->get_user_id_object(bot_user_id, "RequestUrlAuthQuery"),
474 request->request_write_access_));
475 break;
476 }
477 case telegram_api::urlAuthResultAccepted::ID: {
478 auto accepted = telegram_api::move_object_as<telegram_api::urlAuthResultAccepted>(result);
479 promise_.set_value(td_api::make_object<td_api::loginUrlInfoOpen>(accepted->url_, true));
480 break;
481 }
482 case telegram_api::urlAuthResultDefault::ID:
483 promise_.set_value(td_api::make_object<td_api::loginUrlInfoOpen>(url_, false));
484 break;
485 }
486 }
487
on_error(Status status)488 void on_error(Status status) final {
489 if (!dialog_id_.is_valid() ||
490 !td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "RequestUrlAuthQuery")) {
491 LOG(INFO) << "Receive error for RequestUrlAuthQuery: " << status;
492 }
493 promise_.set_value(td_api::make_object<td_api::loginUrlInfoOpen>(url_, false));
494 }
495 };
496
497 class AcceptUrlAuthQuery final : public Td::ResultHandler {
498 Promise<td_api::object_ptr<td_api::httpUrl>> promise_;
499 string url_;
500 DialogId dialog_id_;
501
502 public:
AcceptUrlAuthQuery(Promise<td_api::object_ptr<td_api::httpUrl>> && promise)503 explicit AcceptUrlAuthQuery(Promise<td_api::object_ptr<td_api::httpUrl>> &&promise) : promise_(std::move(promise)) {
504 }
505
send(string url,FullMessageId full_message_id,int32 button_id,bool allow_write_access)506 void send(string url, FullMessageId full_message_id, int32 button_id, bool allow_write_access) {
507 url_ = std::move(url);
508 int32 flags = 0;
509 tl_object_ptr<telegram_api::InputPeer> input_peer;
510 if (full_message_id.get_dialog_id().is_valid()) {
511 dialog_id_ = full_message_id.get_dialog_id();
512 input_peer = td_->messages_manager_->get_input_peer(dialog_id_, AccessRights::Read);
513 CHECK(input_peer != nullptr);
514 flags |= telegram_api::messages_acceptUrlAuth::PEER_MASK;
515 } else {
516 flags |= telegram_api::messages_acceptUrlAuth::URL_MASK;
517 }
518 if (allow_write_access) {
519 flags |= telegram_api::messages_acceptUrlAuth::WRITE_ALLOWED_MASK;
520 }
521 send_query(G()->net_query_creator().create(telegram_api::messages_acceptUrlAuth(
522 flags, false /*ignored*/, std::move(input_peer), full_message_id.get_message_id().get_server_message_id().get(),
523 button_id, url_)));
524 }
525
on_result(BufferSlice packet)526 void on_result(BufferSlice packet) final {
527 auto result_ptr = fetch_result<telegram_api::messages_acceptUrlAuth>(packet);
528 if (result_ptr.is_error()) {
529 return on_error(result_ptr.move_as_error());
530 }
531
532 auto result = result_ptr.move_as_ok();
533 LOG(INFO) << "Receive " << to_string(result);
534 switch (result->get_id()) {
535 case telegram_api::urlAuthResultRequest::ID:
536 LOG(ERROR) << "Receive unexpected " << to_string(result);
537 return on_error(Status::Error(500, "Receive unexpected urlAuthResultRequest"));
538 case telegram_api::urlAuthResultAccepted::ID: {
539 auto accepted = telegram_api::move_object_as<telegram_api::urlAuthResultAccepted>(result);
540 promise_.set_value(td_api::make_object<td_api::httpUrl>(accepted->url_));
541 break;
542 }
543 case telegram_api::urlAuthResultDefault::ID:
544 promise_.set_value(td_api::make_object<td_api::httpUrl>(url_));
545 break;
546 }
547 }
548
on_error(Status status)549 void on_error(Status status) final {
550 if (!dialog_id_.is_valid() ||
551 !td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "AcceptUrlAuthQuery")) {
552 LOG(INFO) << "Receive error for AcceptUrlAuthQuery: " << status;
553 }
554 promise_.set_error(std::move(status));
555 }
556 };
557
LinkManager(Td * td,ActorShared<> parent)558 LinkManager::LinkManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) {
559 }
560
561 LinkManager::~LinkManager() = default;
562
start_up()563 void LinkManager::start_up() {
564 autologin_update_time_ = Time::now() - 365 * 86400;
565 autologin_domains_ = full_split(G()->td_db()->get_binlog_pmc()->get("autologin_domains"), '\xFF');
566
567 url_auth_domains_ = full_split(G()->td_db()->get_binlog_pmc()->get("url_auth_domains"), '\xFF');
568 }
569
tear_down()570 void LinkManager::tear_down() {
571 parent_.reset();
572 }
573
tolower_begins_with(Slice str,Slice prefix)574 static bool tolower_begins_with(Slice str, Slice prefix) {
575 if (prefix.size() > str.size()) {
576 return false;
577 }
578 for (size_t i = 0; i < prefix.size(); i++) {
579 if (to_lower(str[i]) != prefix[i]) {
580 return false;
581 }
582 }
583 return true;
584 }
585
check_link(Slice link)586 Result<string> LinkManager::check_link(Slice link) {
587 bool is_tg = false;
588 bool is_ton = false;
589 if (tolower_begins_with(link, "tg:")) {
590 link.remove_prefix(3);
591 is_tg = true;
592 } else if (tolower_begins_with(link, "ton:")) {
593 link.remove_prefix(4);
594 is_ton = true;
595 }
596 if ((is_tg || is_ton) && begins_with(link, "//")) {
597 link.remove_prefix(2);
598 }
599 TRY_RESULT(http_url, parse_url(link));
600 if (is_tg || is_ton) {
601 if (tolower_begins_with(link, "http://") || http_url.protocol_ == HttpUrl::Protocol::Https ||
602 !http_url.userinfo_.empty() || http_url.specified_port_ != 0 || http_url.is_ipv6_) {
603 return Status::Error(is_tg ? Slice("Wrong tg URL") : Slice("Wrong ton URL"));
604 }
605
606 Slice query(http_url.query_);
607 CHECK(query[0] == '/');
608 if (query.size() > 1 && query[1] == '?') {
609 query.remove_prefix(1);
610 }
611 for (auto c : http_url.host_) {
612 if (!is_alnum(c) && c != '-' && c != '_') {
613 return Status::Error("Unallowed characters in URL host");
614 }
615 }
616 return PSTRING() << (is_tg ? "tg" : "ton") << "://" << http_url.host_ << query;
617 }
618
619 if (http_url.host_.find('.') == string::npos && !http_url.is_ipv6_) {
620 return Status::Error("Wrong HTTP URL");
621 }
622 return http_url.get_url();
623 }
624
get_link_info(Slice link)625 LinkManager::LinkInfo LinkManager::get_link_info(Slice link) {
626 LinkInfo result;
627 if (link.empty()) {
628 return result;
629 }
630 link.truncate(link.find('#'));
631
632 bool is_tg = false;
633 if (tolower_begins_with(link, "tg:")) {
634 link.remove_prefix(3);
635 if (begins_with(link, "//")) {
636 link.remove_prefix(2);
637 }
638 is_tg = true;
639 }
640
641 auto r_http_url = parse_url(link);
642 if (r_http_url.is_error()) {
643 return result;
644 }
645 auto http_url = r_http_url.move_as_ok();
646
647 if (!http_url.userinfo_.empty() || http_url.is_ipv6_) {
648 return result;
649 }
650
651 if (is_tg) {
652 if (tolower_begins_with(link, "http://") || http_url.protocol_ == HttpUrl::Protocol::Https ||
653 http_url.specified_port_ != 0) {
654 return result;
655 }
656
657 result.is_internal_ = true;
658 result.is_tg_ = true;
659 result.query_ = link.str();
660 return result;
661 } else {
662 if (http_url.port_ != 80 && http_url.port_ != 443) {
663 return result;
664 }
665
666 vector<Slice> t_me_urls{Slice("t.me"), Slice("telegram.me"), Slice("telegram.dog")};
667 if (Scheduler::context() != nullptr) { // for tests only
668 string cur_t_me_url = G()->shared_config().get_option_string("t_me_url");
669 if (tolower_begins_with(cur_t_me_url, "http://") || tolower_begins_with(cur_t_me_url, "https://")) {
670 Slice t_me_url = cur_t_me_url;
671 t_me_url = t_me_url.substr(t_me_url[4] == 's' ? 8 : 7);
672 if (!td::contains(t_me_urls, t_me_url)) {
673 t_me_urls.push_back(t_me_url);
674 }
675 }
676 }
677
678 auto host = url_decode(http_url.host_, false);
679 to_lower_inplace(host);
680 if (begins_with(host, "www.")) {
681 host = host.substr(4);
682 }
683
684 for (auto t_me_url : t_me_urls) {
685 if (host == t_me_url) {
686 result.is_internal_ = true;
687 result.is_tg_ = false;
688
689 Slice query = http_url.query_;
690 while (true) {
691 if (begins_with(query, "/s/")) {
692 query.remove_prefix(2);
693 continue;
694 }
695 if (begins_with(query, "/%73/")) {
696 query.remove_prefix(4);
697 continue;
698 }
699 break;
700 }
701 result.query_ = query.str();
702 return result;
703 }
704 }
705 }
706 return result;
707 }
708
parse_internal_link(Slice link)709 unique_ptr<LinkManager::InternalLink> LinkManager::parse_internal_link(Slice link) {
710 auto info = get_link_info(link);
711 if (!info.is_internal_) {
712 return nullptr;
713 }
714 if (info.is_tg_) {
715 return parse_tg_link_query(info.query_);
716 } else {
717 return parse_t_me_link_query(info.query_);
718 }
719 }
720
721 namespace {
722 struct CopyArg {
723 Slice name_;
724 const HttpUrlQuery *url_query_;
725 bool *is_first_;
726
CopyArgtd::__anone586a99a0211::CopyArg727 CopyArg(Slice name, const HttpUrlQuery *url_query, bool *is_first)
728 : name_(name), url_query_(url_query), is_first_(is_first) {
729 }
730 };
731
operator <<(StringBuilder & string_builder,const CopyArg & copy_arg)732 StringBuilder &operator<<(StringBuilder &string_builder, const CopyArg ©_arg) {
733 auto arg = copy_arg.url_query_->get_arg(copy_arg.name_);
734 if (arg.empty()) {
735 for (const auto &query_arg : copy_arg.url_query_->args_) {
736 if (query_arg.first == copy_arg.name_) {
737 char c = *copy_arg.is_first_ ? '?' : '&';
738 *copy_arg.is_first_ = false;
739 return string_builder << c << copy_arg.name_;
740 }
741 }
742 return string_builder;
743 }
744 char c = *copy_arg.is_first_ ? '?' : '&';
745 *copy_arg.is_first_ = false;
746 return string_builder << c << copy_arg.name_ << '=' << url_encode(arg);
747 }
748 } // namespace
749
parse_tg_link_query(Slice query)750 unique_ptr<LinkManager::InternalLink> LinkManager::parse_tg_link_query(Slice query) {
751 const auto url_query = parse_url_query(query);
752 const auto &path = url_query.path_;
753
754 bool is_first_arg = true;
755 auto copy_arg = [&](Slice name) {
756 return CopyArg(name, &url_query, &is_first_arg);
757 };
758 auto pass_arg = [&](Slice name) {
759 return url_encode(url_query.get_arg(name));
760 };
761 auto get_arg = [&](Slice name) {
762 return url_query.get_arg(name).str();
763 };
764 auto has_arg = [&](Slice name) {
765 return !url_query.get_arg(name).empty();
766 };
767
768 if (path.size() == 1 && path[0] == "resolve") {
769 if (is_valid_username(get_arg("domain"))) {
770 if (has_arg("post")) {
771 // resolve?domain=<username>&post=12345&single&thread=<thread_id>&comment=<message_id>&t=<media_timestamp>
772 return td::make_unique<InternalLinkMessage>(PSTRING() << "tg:resolve" << copy_arg("domain") << copy_arg("post")
773 << copy_arg("single") << copy_arg("thread")
774 << copy_arg("comment") << copy_arg("t"));
775 }
776 auto username = get_arg("domain");
777 for (auto &arg : url_query.args_) {
778 if (arg.first == "voicechat" || arg.first == "videochat" || arg.first == "livestream") {
779 // resolve?domain=<username>&videochat
780 // resolve?domain=<username>&videochat=<invite_hash>
781 if (Scheduler::context() != nullptr) {
782 send_closure(G()->messages_manager(), &MessagesManager::reload_voice_chat_on_search, username);
783 }
784 return td::make_unique<InternalLinkVoiceChat>(std::move(username), arg.second, arg.first == "livestream");
785 }
786 if (arg.first == "start" && is_valid_start_parameter(arg.second)) {
787 // resolve?domain=<bot_username>?start=<parameter>
788 return td::make_unique<InternalLinkBotStart>(std::move(username), arg.second);
789 }
790 if (arg.first == "startgroup" && is_valid_start_parameter(arg.second)) {
791 // resolve?domain=<bot_username>?startgroup=<parameter>
792 return td::make_unique<InternalLinkBotStartInGroup>(std::move(username), arg.second);
793 }
794 if (arg.first == "game" && !arg.second.empty()) {
795 // resolve?domain=<bot_username>?game=<short_name>
796 return td::make_unique<InternalLinkGame>(std::move(username), arg.second);
797 }
798 }
799 if (username == "telegrampassport") {
800 // resolve?domain=telegrampassport&bot_id=<bot_user_id>&scope=<scope>&public_key=<public_key>&nonce=<nonce>
801 return get_internal_link_passport(query, url_query.args_);
802 }
803 // resolve?domain=<username>
804 return td::make_unique<InternalLinkPublicDialog>(std::move(username));
805 }
806 } else if (path.size() == 1 && path[0] == "login") {
807 // login?code=123456
808 if (has_arg("code")) {
809 return td::make_unique<InternalLinkAuthenticationCode>(get_arg("code"));
810 }
811 // login?token=<token>
812 if (has_arg("token")) {
813 return td::make_unique<InternalLinkQrCodeAuthentication>();
814 }
815 } else if (path.size() == 1 && path[0] == "passport") {
816 // passport?bot_id=<bot_user_id>&scope=<scope>&public_key=<public_key>&nonce=<nonce>
817 return get_internal_link_passport(query, url_query.args_);
818 } else if (!path.empty() && path[0] == "settings") {
819 if (path.size() == 2 && path[1] == "change_number") {
820 // settings/change_number
821 return td::make_unique<InternalLinkChangePhoneNumber>();
822 }
823 if (path.size() == 2 && path[1] == "devices") {
824 // settings/devices
825 return td::make_unique<InternalLinkActiveSessions>();
826 }
827 if (path.size() == 2 && path[1] == "folders") {
828 // settings/folders
829 return td::make_unique<InternalLinkFilterSettings>();
830 }
831 if (path.size() == 2 && path[1] == "themes") {
832 // settings/themes
833 return td::make_unique<InternalLinkThemeSettings>();
834 }
835 // settings
836 return td::make_unique<InternalLinkSettings>();
837 } else if (path.size() == 1 && path[0] == "join") {
838 // join?invite=<hash>
839 if (has_arg("invite")) {
840 return td::make_unique<InternalLinkDialogInvite>(PSTRING() << "tg:join?invite="
841 << url_encode(get_url_query_hash(true, url_query)));
842 }
843 } else if (path.size() == 1 && path[0] == "addstickers") {
844 // addstickers?set=<name>
845 if (has_arg("set")) {
846 return td::make_unique<InternalLinkStickerSet>(get_arg("set"));
847 }
848 } else if (path.size() == 1 && path[0] == "setlanguage") {
849 // setlanguage?lang=<name>
850 if (has_arg("lang")) {
851 return td::make_unique<InternalLinkLanguage>(get_arg("lang"));
852 }
853 } else if (path.size() == 1 && path[0] == "addtheme") {
854 // addtheme?slug=<name>
855 if (has_arg("slug")) {
856 return td::make_unique<InternalLinkTheme>(get_arg("slug"));
857 }
858 } else if (path.size() == 1 && path[0] == "confirmphone") {
859 if (has_arg("hash") && has_arg("phone")) {
860 // confirmphone?phone=<phone>&hash=<hash>
861 return td::make_unique<InternalLinkConfirmPhone>(get_arg("hash"), get_arg("phone"));
862 }
863 } else if (path.size() == 1 && path[0] == "socks") {
864 if (has_arg("server") && has_arg("port")) {
865 // socks?server=<server>&port=<port>&user=<user>&pass=<pass>
866 auto port = to_integer<int32>(get_arg("port"));
867 if (0 < port && port < 65536) {
868 return td::make_unique<InternalLinkProxy>(
869 get_arg("server"), port, td_api::make_object<td_api::proxyTypeSocks5>(get_arg("user"), get_arg("pass")));
870 } else {
871 return td::make_unique<InternalLinkUnsupportedProxy>();
872 }
873 }
874 } else if (path.size() == 1 && path[0] == "proxy") {
875 if (has_arg("server") && has_arg("port")) {
876 // proxy?server=<server>&port=<port>&secret=<secret>
877 auto port = to_integer<int32>(get_arg("port"));
878 if (0 < port && port < 65536 && mtproto::ProxySecret::from_link(get_arg("secret")).is_ok()) {
879 return td::make_unique<InternalLinkProxy>(get_arg("server"), port,
880 td_api::make_object<td_api::proxyTypeMtproto>(get_arg("secret")));
881 } else {
882 return td::make_unique<InternalLinkUnsupportedProxy>();
883 }
884 }
885 } else if (path.size() == 1 && path[0] == "privatepost") {
886 // privatepost?channel=123456789&msg_id=12345&single&thread=<thread_id>&comment=<message_id>&t=<media_timestamp>
887 if (has_arg("channel") && has_arg("msg_id")) {
888 return td::make_unique<InternalLinkMessage>(
889 PSTRING() << "tg:privatepost" << copy_arg("channel") << copy_arg("msg_id") << copy_arg("single")
890 << copy_arg("thread") << copy_arg("comment") << copy_arg("t"));
891 }
892 } else if (path.size() == 1 && path[0] == "bg") {
893 // bg?color=<color>
894 // bg?gradient=<hex_color>-<hex_color>&rotation=...
895 // bg?gradient=<hex_color>~<hex_color>~<hex_color>~<hex_color>
896 // bg?slug=<background_name>&mode=blur+motion
897 // bg?slug=<pattern_name>&intensity=...&bg_color=...&mode=blur+motion
898 if (has_arg("color")) {
899 return td::make_unique<InternalLinkBackground>(pass_arg("color"));
900 }
901 if (has_arg("gradient")) {
902 return td::make_unique<InternalLinkBackground>(PSTRING() << pass_arg("gradient") << copy_arg("rotation"));
903 }
904 if (has_arg("slug")) {
905 return td::make_unique<InternalLinkBackground>(PSTRING()
906 << pass_arg("slug") << copy_arg("mode") << copy_arg("intensity")
907 << copy_arg("bg_color") << copy_arg("rotation"));
908 }
909 } else if (path.size() == 1 && (path[0] == "share" || path[0] == "msg" || path[0] == "msg_url")) {
910 // msg_url?url=<url>
911 // msg_url?url=<url>&text=<text>
912 return get_internal_link_message_draft(get_arg("url"), get_arg("text"));
913 }
914 if (!path.empty() && !path[0].empty()) {
915 return td::make_unique<InternalLinkUnknownDeepLink>(PSTRING() << "tg://" << query);
916 }
917 return nullptr;
918 }
919
parse_t_me_link_query(Slice query)920 unique_ptr<LinkManager::InternalLink> LinkManager::parse_t_me_link_query(Slice query) {
921 CHECK(query[0] == '/');
922 const auto url_query = parse_url_query(query);
923 const auto &path = url_query.path_;
924 if (path.empty() || path[0].empty()) {
925 return nullptr;
926 }
927
928 bool is_first_arg = true;
929 auto copy_arg = [&](Slice name) {
930 return CopyArg(name, &url_query, &is_first_arg);
931 };
932
933 auto get_arg = [&](Slice name) {
934 return url_query.get_arg(name).str();
935 };
936 auto has_arg = [&](Slice name) {
937 return !url_query.get_arg(name).empty();
938 };
939
940 if (path[0] == "c") {
941 if (path.size() >= 3 && to_integer<int64>(path[1]) > 0 && to_integer<int64>(path[2]) > 0) {
942 // /c/123456789/12345?single&thread=<thread_id>&comment=<message_id>&t=<media_timestamp>
943 is_first_arg = false;
944 return td::make_unique<InternalLinkMessage>(PSTRING()
945 << "tg:privatepost?channel=" << to_integer<int64>(path[1])
946 << "&msg_id=" << to_integer<int64>(path[2]) << copy_arg("single")
947 << copy_arg("thread") << copy_arg("comment") << copy_arg("t"));
948 }
949 } else if (path[0] == "login") {
950 if (path.size() >= 2 && !path[1].empty()) {
951 // /login/<code>
952 return td::make_unique<InternalLinkAuthenticationCode>(path[1]);
953 }
954 } else if (path[0] == "joinchat") {
955 if (path.size() >= 2 && !path[1].empty()) {
956 // /joinchat/<link>
957 return td::make_unique<InternalLinkDialogInvite>(PSTRING() << "tg:join?invite="
958 << url_encode(get_url_query_hash(false, url_query)));
959 }
960 } else if (path[0][0] == ' ' || path[0][0] == '+') {
961 if (path[0].size() >= 2) {
962 // /+<link>
963 return td::make_unique<InternalLinkDialogInvite>(PSTRING() << "tg:join?invite="
964 << url_encode(get_url_query_hash(false, url_query)));
965 }
966 } else if (path[0] == "addstickers") {
967 if (path.size() >= 2 && !path[1].empty()) {
968 // /addstickers/<name>
969 return td::make_unique<InternalLinkStickerSet>(path[1]);
970 }
971 } else if (path[0] == "setlanguage") {
972 if (path.size() >= 2 && !path[1].empty()) {
973 // /setlanguage/<name>
974 return td::make_unique<InternalLinkLanguage>(path[1]);
975 }
976 } else if (path[0] == "addtheme") {
977 if (path.size() >= 2 && !path[1].empty()) {
978 // /addtheme/<name>
979 return td::make_unique<InternalLinkTheme>(path[1]);
980 }
981 } else if (path[0] == "confirmphone") {
982 if (has_arg("hash") && has_arg("phone")) {
983 // /confirmphone?phone=<phone>&hash=<hash>
984 return td::make_unique<InternalLinkConfirmPhone>(get_arg("hash"), get_arg("phone"));
985 }
986 } else if (path[0] == "socks") {
987 if (has_arg("server") && has_arg("port")) {
988 // /socks?server=<server>&port=<port>&user=<user>&pass=<pass>
989 auto port = to_integer<int32>(get_arg("port"));
990 if (0 < port && port < 65536) {
991 return td::make_unique<InternalLinkProxy>(
992 get_arg("server"), port, td_api::make_object<td_api::proxyTypeSocks5>(get_arg("user"), get_arg("pass")));
993 } else {
994 return td::make_unique<InternalLinkUnsupportedProxy>();
995 }
996 }
997 } else if (path[0] == "proxy") {
998 if (has_arg("server") && has_arg("port")) {
999 // /proxy?server=<server>&port=<port>&secret=<secret>
1000 auto port = to_integer<int32>(get_arg("port"));
1001 if (0 < port && port < 65536 && mtproto::ProxySecret::from_link(get_arg("secret")).is_ok()) {
1002 return td::make_unique<InternalLinkProxy>(get_arg("server"), port,
1003 td_api::make_object<td_api::proxyTypeMtproto>(get_arg("secret")));
1004 } else {
1005 return td::make_unique<InternalLinkUnsupportedProxy>();
1006 }
1007 }
1008 } else if (path[0] == "bg") {
1009 if (path.size() >= 2 && !path[1].empty()) {
1010 // /bg/<hex_color>
1011 // /bg/<hex_color>-<hex_color>?rotation=...
1012 // /bg/<hex_color>~<hex_color>~<hex_color>~<hex_color>
1013 // /bg/<background_name>?mode=blur+motion
1014 // /bg/<pattern_name>?intensity=...&bg_color=...&mode=blur+motion
1015 return td::make_unique<InternalLinkBackground>(PSTRING()
1016 << url_encode(path[1]) << copy_arg("mode") << copy_arg("intensity")
1017 << copy_arg("bg_color") << copy_arg("rotation"));
1018 }
1019 } else if (path[0] == "share" || path[0] == "msg") {
1020 if (!(path.size() > 1 && (path[1] == "bookmarklet" || path[1] == "embed"))) {
1021 // /share?url=<url>
1022 // /share/url?url=<url>&text=<text>
1023 return get_internal_link_message_draft(get_arg("url"), get_arg("text"));
1024 }
1025 } else if (is_valid_username(path[0])) {
1026 if (path.size() >= 2 && to_integer<int64>(path[1]) > 0) {
1027 // /<username>/12345?single&thread=<thread_id>&comment=<message_id>&t=<media_timestamp>
1028 is_first_arg = false;
1029 return td::make_unique<InternalLinkMessage>(
1030 PSTRING() << "tg:resolve?domain=" << url_encode(path[0]) << "&post=" << to_integer<int64>(path[1])
1031 << copy_arg("single") << copy_arg("thread") << copy_arg("comment") << copy_arg("t"));
1032 }
1033 auto username = path[0];
1034 for (auto &arg : url_query.args_) {
1035 if (arg.first == "voicechat" || arg.first == "videochat" || arg.first == "livestream") {
1036 // /<username>?videochat
1037 // /<username>?videochat=<invite_hash>
1038 if (Scheduler::context() != nullptr) {
1039 send_closure(G()->messages_manager(), &MessagesManager::reload_voice_chat_on_search, username);
1040 }
1041 return td::make_unique<InternalLinkVoiceChat>(std::move(username), arg.second, arg.first == "livestream");
1042 }
1043 if (arg.first == "start" && is_valid_start_parameter(arg.second)) {
1044 // /<bot_username>?start=<parameter>
1045 return td::make_unique<InternalLinkBotStart>(std::move(username), arg.second);
1046 }
1047 if (arg.first == "startgroup" && is_valid_start_parameter(arg.second)) {
1048 // /<bot_username>?startgroup=<parameter>
1049 return td::make_unique<InternalLinkBotStartInGroup>(std::move(username), arg.second);
1050 }
1051 if (arg.first == "game" && !arg.second.empty()) {
1052 // /<bot_username>?game=<short_name>
1053 return td::make_unique<InternalLinkGame>(std::move(username), arg.second);
1054 }
1055 }
1056 // /<username>
1057 return td::make_unique<InternalLinkPublicDialog>(std::move(username));
1058 }
1059 return nullptr;
1060 }
1061
get_internal_link_message_draft(Slice url,Slice text)1062 unique_ptr<LinkManager::InternalLink> LinkManager::get_internal_link_message_draft(Slice url, Slice text) {
1063 if (url.empty() && text.empty()) {
1064 return nullptr;
1065 }
1066 while (!text.empty() && text.back() == '\n') {
1067 text.remove_suffix(1);
1068 }
1069 url = trim(url);
1070 if (url.empty()) {
1071 url = text;
1072 text = Slice();
1073 }
1074 FormattedText full_text;
1075 bool contains_url = false;
1076 if (!text.empty()) {
1077 contains_url = true;
1078 full_text.text = PSTRING() << url << '\n' << text;
1079 } else {
1080 full_text.text = url.str();
1081 }
1082 if (fix_formatted_text(full_text.text, full_text.entities, false, false, false, true, true).is_error()) {
1083 return nullptr;
1084 }
1085 if (full_text.text[0] == '@') {
1086 full_text.text = ' ' + full_text.text;
1087 for (auto &entity : full_text.entities) {
1088 entity.offset++;
1089 }
1090 }
1091 return td::make_unique<InternalLinkMessageDraft>(std::move(full_text), contains_url);
1092 }
1093
get_internal_link_passport(Slice query,const vector<std::pair<string,string>> & args)1094 unique_ptr<LinkManager::InternalLink> LinkManager::get_internal_link_passport(
1095 Slice query, const vector<std::pair<string, string>> &args) {
1096 auto get_arg = [&args](Slice key) {
1097 for (auto &arg : args) {
1098 if (arg.first == key) {
1099 return Slice(arg.second);
1100 }
1101 }
1102 return Slice();
1103 };
1104
1105 UserId bot_user_id(to_integer<int64>(get_arg("bot_id")));
1106 auto scope = get_arg("scope");
1107 auto public_key = get_arg("public_key");
1108 auto nonce = get_arg("nonce");
1109 if (nonce.empty()) {
1110 nonce = get_arg("payload");
1111 }
1112 auto callback_url = get_arg("callback_url");
1113
1114 if (!bot_user_id.is_valid() || scope.empty() || public_key.empty() || nonce.empty()) {
1115 return td::make_unique<InternalLinkUnknownDeepLink>(PSTRING() << "tg://" << query);
1116 }
1117 return td::make_unique<InternalLinkPassportDataRequest>(bot_user_id, scope.str(), public_key.str(), nonce.str(),
1118 callback_url.str());
1119 }
1120
update_autologin_domains(string autologin_token,vector<string> autologin_domains,vector<string> url_auth_domains)1121 void LinkManager::update_autologin_domains(string autologin_token, vector<string> autologin_domains,
1122 vector<string> url_auth_domains) {
1123 autologin_update_time_ = Time::now();
1124 autologin_token_ = std::move(autologin_token);
1125 if (autologin_domains_ != autologin_domains) {
1126 autologin_domains_ = std::move(autologin_domains);
1127 G()->td_db()->get_binlog_pmc()->set("autologin_domains", implode(autologin_domains_, '\xFF'));
1128 }
1129 if (url_auth_domains_ != url_auth_domains) {
1130 url_auth_domains_ = std::move(url_auth_domains);
1131 G()->td_db()->get_binlog_pmc()->set("url_auth_domains", implode(url_auth_domains_, '\xFF'));
1132 }
1133 }
1134
get_deep_link_info(Slice link,Promise<td_api::object_ptr<td_api::deepLinkInfo>> && promise)1135 void LinkManager::get_deep_link_info(Slice link, Promise<td_api::object_ptr<td_api::deepLinkInfo>> &&promise) {
1136 Slice link_scheme("tg:");
1137 if (begins_with(link, link_scheme)) {
1138 link.remove_prefix(link_scheme.size());
1139 if (begins_with(link, "//")) {
1140 link.remove_prefix(2);
1141 }
1142 }
1143 size_t pos = 0;
1144 while (pos < link.size() && link[pos] != '/' && link[pos] != '?' && link[pos] != '#') {
1145 pos++;
1146 }
1147 link.truncate(pos);
1148 td_->create_handler<GetDeepLinkInfoQuery>(std::move(promise))->send(link);
1149 }
1150
get_external_link_info(string && link,Promise<td_api::object_ptr<td_api::LoginUrlInfo>> && promise)1151 void LinkManager::get_external_link_info(string &&link, Promise<td_api::object_ptr<td_api::LoginUrlInfo>> &&promise) {
1152 auto default_result = td_api::make_object<td_api::loginUrlInfoOpen>(link, false);
1153 if (G()->close_flag()) {
1154 return promise.set_value(std::move(default_result));
1155 }
1156
1157 auto r_url = parse_url(link);
1158 if (r_url.is_error()) {
1159 return promise.set_value(std::move(default_result));
1160 }
1161
1162 if (!td::contains(autologin_domains_, r_url.ok().host_)) {
1163 if (td::contains(url_auth_domains_, r_url.ok().host_)) {
1164 td_->create_handler<RequestUrlAuthQuery>(std::move(promise))->send(link, FullMessageId(), 0);
1165 return;
1166 }
1167 return promise.set_value(std::move(default_result));
1168 }
1169
1170 if (autologin_update_time_ < Time::now() - 10000) {
1171 auto query_promise =
1172 PromiseCreator::lambda([link = std::move(link), promise = std::move(promise)](Result<Unit> &&result) mutable {
1173 if (result.is_error()) {
1174 return promise.set_value(td_api::make_object<td_api::loginUrlInfoOpen>(link, false));
1175 }
1176 send_closure(G()->link_manager(), &LinkManager::get_external_link_info, std::move(link), std::move(promise));
1177 });
1178 return send_closure(G()->config_manager(), &ConfigManager::reget_app_config, std::move(query_promise));
1179 }
1180
1181 if (autologin_token_.empty()) {
1182 return promise.set_value(std::move(default_result));
1183 }
1184
1185 auto url = r_url.move_as_ok();
1186 url.protocol_ = HttpUrl::Protocol::Https;
1187 Slice path = url.query_;
1188 path.truncate(url.query_.find_first_of("?#"));
1189 Slice parameters_hash = Slice(url.query_).substr(path.size());
1190 Slice parameters = parameters_hash;
1191 parameters.truncate(parameters.find('#'));
1192 Slice hash = parameters_hash.substr(parameters.size());
1193
1194 string added_parameter;
1195 if (parameters.empty()) {
1196 added_parameter = '?';
1197 } else if (parameters.size() == 1) {
1198 CHECK(parameters == "?");
1199 } else {
1200 added_parameter = '&';
1201 }
1202 added_parameter += "autologin_token=";
1203 added_parameter += autologin_token_;
1204
1205 url.query_ = PSTRING() << path << parameters << added_parameter << hash;
1206
1207 promise.set_value(td_api::make_object<td_api::loginUrlInfoOpen>(url.get_url(), false));
1208 }
1209
get_login_url_info(FullMessageId full_message_id,int64 button_id,Promise<td_api::object_ptr<td_api::LoginUrlInfo>> && promise)1210 void LinkManager::get_login_url_info(FullMessageId full_message_id, int64 button_id,
1211 Promise<td_api::object_ptr<td_api::LoginUrlInfo>> &&promise) {
1212 TRY_RESULT_PROMISE(promise, url, td_->messages_manager_->get_login_button_url(full_message_id, button_id));
1213 td_->create_handler<RequestUrlAuthQuery>(std::move(promise))
1214 ->send(std::move(url), full_message_id, narrow_cast<int32>(button_id));
1215 }
1216
get_login_url(FullMessageId full_message_id,int64 button_id,bool allow_write_access,Promise<td_api::object_ptr<td_api::httpUrl>> && promise)1217 void LinkManager::get_login_url(FullMessageId full_message_id, int64 button_id, bool allow_write_access,
1218 Promise<td_api::object_ptr<td_api::httpUrl>> &&promise) {
1219 TRY_RESULT_PROMISE(promise, url, td_->messages_manager_->get_login_button_url(full_message_id, button_id));
1220 td_->create_handler<AcceptUrlAuthQuery>(std::move(promise))
1221 ->send(std::move(url), full_message_id, narrow_cast<int32>(button_id), allow_write_access);
1222 }
1223
get_link_login_url(const string & url,bool allow_write_access,Promise<td_api::object_ptr<td_api::httpUrl>> && promise)1224 void LinkManager::get_link_login_url(const string &url, bool allow_write_access,
1225 Promise<td_api::object_ptr<td_api::httpUrl>> &&promise) {
1226 td_->create_handler<AcceptUrlAuthQuery>(std::move(promise))->send(url, FullMessageId(), 0, allow_write_access);
1227 }
1228
get_dialog_invite_link_hash(Slice invite_link)1229 string LinkManager::get_dialog_invite_link_hash(Slice invite_link) {
1230 auto link_info = get_link_info(invite_link);
1231 if (!link_info.is_internal_) {
1232 return string();
1233 }
1234 const auto url_query = parse_url_query(link_info.query_);
1235 return get_url_query_hash(link_info.is_tg_, url_query);
1236 }
1237
get_link_user_id(Slice url)1238 UserId LinkManager::get_link_user_id(Slice url) {
1239 string lower_cased_url = to_lower(url);
1240 url = lower_cased_url;
1241
1242 Slice link_scheme("tg:");
1243 if (!begins_with(url, link_scheme)) {
1244 return UserId();
1245 }
1246 url.remove_prefix(link_scheme.size());
1247 if (begins_with(url, "//")) {
1248 url.remove_prefix(2);
1249 }
1250
1251 Slice host("user");
1252 if (!begins_with(url, host)) {
1253 return UserId();
1254 }
1255 url.remove_prefix(host.size());
1256 if (begins_with(url, "/")) {
1257 url.remove_prefix(1);
1258 }
1259 if (!begins_with(url, "?")) {
1260 return UserId();
1261 }
1262 url.remove_prefix(1);
1263 url.truncate(url.find('#'));
1264
1265 for (auto parameter : full_split(url, '&')) {
1266 Slice key;
1267 Slice value;
1268 std::tie(key, value) = split(parameter, '=');
1269 if (key == Slice("id")) {
1270 auto r_user_id = to_integer_safe<int64>(value);
1271 if (r_user_id.is_error()) {
1272 return UserId();
1273 }
1274 return UserId(r_user_id.ok());
1275 }
1276 }
1277 return UserId();
1278 }
1279
get_message_link_info(Slice url)1280 Result<MessageLinkInfo> LinkManager::get_message_link_info(Slice url) {
1281 if (url.empty()) {
1282 return Status::Error("URL must be non-empty");
1283 }
1284 auto link_info = get_link_info(url);
1285 if (!link_info.is_internal_) {
1286 return Status::Error("Invalid message link URL");
1287 }
1288 url = link_info.query_;
1289
1290 Slice username;
1291 Slice channel_id_slice;
1292 Slice message_id_slice;
1293 Slice comment_message_id_slice = "0";
1294 Slice media_timestamp_slice;
1295 bool is_single = false;
1296 bool for_comment = false;
1297 if (link_info.is_tg_) {
1298 // resolve?domain=username&post=12345&single&t=123&comment=12&thread=21
1299 // privatepost?channel=123456789&msg_id=12345&single&t=123&comment=12&thread=21
1300
1301 bool is_resolve = false;
1302 if (begins_with(url, "resolve")) {
1303 url = url.substr(7);
1304 is_resolve = true;
1305 } else if (begins_with(url, "privatepost")) {
1306 url = url.substr(11);
1307 } else {
1308 return Status::Error("Wrong message link URL");
1309 }
1310
1311 if (begins_with(url, "/")) {
1312 url = url.substr(1);
1313 }
1314 if (!begins_with(url, "?")) {
1315 return Status::Error("Wrong message link URL");
1316 }
1317 url = url.substr(1);
1318
1319 auto args = full_split(url, '&');
1320 for (auto arg : args) {
1321 auto key_value = split(arg, '=');
1322 if (is_resolve) {
1323 if (key_value.first == "domain") {
1324 username = key_value.second;
1325 }
1326 if (key_value.first == "post") {
1327 message_id_slice = key_value.second;
1328 }
1329 } else {
1330 if (key_value.first == "channel") {
1331 channel_id_slice = key_value.second;
1332 }
1333 if (key_value.first == "msg_id") {
1334 message_id_slice = key_value.second;
1335 }
1336 }
1337 if (key_value.first == "t") {
1338 media_timestamp_slice = key_value.second;
1339 }
1340 if (key_value.first == "single") {
1341 is_single = true;
1342 }
1343 if (key_value.first == "comment") {
1344 comment_message_id_slice = key_value.second;
1345 }
1346 if (key_value.first == "thread") {
1347 for_comment = true;
1348 }
1349 }
1350 } else {
1351 // /c/123456789/12345
1352 // /username/12345?single
1353
1354 CHECK(!url.empty() && url[0] == '/');
1355 url.remove_prefix(1);
1356
1357 auto username_end_pos = url.find('/');
1358 if (username_end_pos == Slice::npos) {
1359 return Status::Error("Wrong message link URL");
1360 }
1361 username = url.substr(0, username_end_pos);
1362 url = url.substr(username_end_pos + 1);
1363 if (username == "c") {
1364 username = Slice();
1365 auto channel_id_end_pos = url.find('/');
1366 if (channel_id_end_pos == Slice::npos) {
1367 return Status::Error("Wrong message link URL");
1368 }
1369 channel_id_slice = url.substr(0, channel_id_end_pos);
1370 url = url.substr(channel_id_end_pos + 1);
1371 }
1372
1373 auto query_pos = url.find('?');
1374 message_id_slice = url.substr(0, query_pos);
1375 if (query_pos != Slice::npos) {
1376 auto args = full_split(url.substr(query_pos + 1), '&');
1377 for (auto arg : args) {
1378 auto key_value = split(arg, '=');
1379 if (key_value.first == "t") {
1380 media_timestamp_slice = key_value.second;
1381 }
1382 if (key_value.first == "single") {
1383 is_single = true;
1384 }
1385 if (key_value.first == "comment") {
1386 comment_message_id_slice = key_value.second;
1387 }
1388 if (key_value.first == "thread") {
1389 for_comment = true;
1390 }
1391 }
1392 }
1393 }
1394
1395 ChannelId channel_id;
1396 if (username.empty()) {
1397 auto r_channel_id = to_integer_safe<int64>(channel_id_slice);
1398 if (r_channel_id.is_error() || !ChannelId(r_channel_id.ok()).is_valid()) {
1399 return Status::Error("Wrong channel ID");
1400 }
1401 channel_id = ChannelId(r_channel_id.ok());
1402 }
1403
1404 auto r_message_id = to_integer_safe<int32>(message_id_slice);
1405 if (r_message_id.is_error() || !ServerMessageId(r_message_id.ok()).is_valid()) {
1406 return Status::Error("Wrong message ID");
1407 }
1408
1409 auto r_comment_message_id = to_integer_safe<int32>(comment_message_id_slice);
1410 if (r_comment_message_id.is_error() ||
1411 !(r_comment_message_id.ok() == 0 || ServerMessageId(r_comment_message_id.ok()).is_valid())) {
1412 return Status::Error("Wrong comment message ID");
1413 }
1414
1415 bool is_media_timestamp_invalid = false;
1416 int32 media_timestamp = 0;
1417 const int32 MAX_MEDIA_TIMESTAMP = 10000000;
1418 if (!media_timestamp_slice.empty()) {
1419 int32 current_value = 0;
1420 for (size_t i = 0; i <= media_timestamp_slice.size(); i++) {
1421 auto c = i < media_timestamp_slice.size() ? media_timestamp_slice[i] : 's';
1422 if ('0' <= c && c <= '9') {
1423 current_value = current_value * 10 + c - '0';
1424 if (current_value > MAX_MEDIA_TIMESTAMP) {
1425 is_media_timestamp_invalid = true;
1426 break;
1427 }
1428 } else {
1429 auto mul = 0;
1430 switch (to_lower(c)) {
1431 case 'h':
1432 mul = 3600;
1433 break;
1434 case 'm':
1435 mul = 60;
1436 break;
1437 case 's':
1438 mul = 1;
1439 break;
1440 }
1441 if (mul == 0 || current_value > MAX_MEDIA_TIMESTAMP / mul ||
1442 media_timestamp + current_value * mul > MAX_MEDIA_TIMESTAMP) {
1443 is_media_timestamp_invalid = true;
1444 break;
1445 }
1446 media_timestamp += current_value * mul;
1447 current_value = 0;
1448 }
1449 }
1450 }
1451
1452 MessageLinkInfo info;
1453 info.username = username.str();
1454 info.channel_id = channel_id;
1455 info.message_id = MessageId(ServerMessageId(r_message_id.ok()));
1456 info.comment_message_id = MessageId(ServerMessageId(r_comment_message_id.ok()));
1457 info.media_timestamp = is_media_timestamp_invalid ? 0 : media_timestamp;
1458 info.is_single = is_single;
1459 info.for_comment = for_comment;
1460 LOG(INFO) << "Have link to " << info.message_id << " in chat @" << info.username << "/" << channel_id.get();
1461 return std::move(info);
1462 }
1463
1464 } // namespace td
1465