Merge remote-tracking branch 'td/master'

This commit is contained in:
Andrea Cavalli 2021-12-14 20:25:59 +01:00
commit 3d913fcb2e
8 changed files with 86 additions and 63 deletions

View File

@ -310,7 +310,7 @@ contact phone_number:string first_name:string last_name:string vcard:string user
//@horizontal_accuracy The estimated horizontal accuracy of the location, in meters; as defined by the sender. 0 if unknown
location latitude:double longitude:double horizontal_accuracy:double = Location;
//@description Describes a venue @location Venue location; as defined by the sender @title Venue name; as defined by the sender @address Venue address; as defined by the sender @provider Provider of the venue database; as defined by the sender. Currently only "foursquare" and "gplaces" (Google Places) need to be supported
//@description Describes a venue @location Venue location; as defined by the sender @title Venue name; as defined by the sender @address Venue address; as defined by the sender @provider Provider of the venue database; as defined by the sender. Currently, only "foursquare" and "gplaces" (Google Places) need to be supported
//@id Identifier of the venue in the provider database; as defined by the sender @type Type of the venue in the provider database; as defined by the sender
venue location:location title:string address:string provider:string id:string type:string = Venue;
@ -655,7 +655,7 @@ basicGroupFullInfo photo:chatPhoto description:string creator_user_id:int53 memb
//@username Username of the supergroup or channel; empty for private supergroups or channels
//@date Point in time (Unix timestamp) when the current user joined, or the point in time when the supergroup or channel was created, in case the user is not a member
//@status Status of the current user in the supergroup or channel; custom title will be always empty
//@member_count Number of members in the supergroup or channel; 0 if unknown. Currently it is guaranteed to be known only if the supergroup or channel was received through searchPublicChats, searchChatsNearby, getInactiveSupergroupChats, getSuitableDiscussionChats, getGroupsInCommon, or getUserPrivacySettingRules
//@member_count Number of members in the supergroup or channel; 0 if unknown. Currently, it is guaranteed to be known only if the supergroup or channel was received through searchPublicChats, searchChatsNearby, getInactiveSupergroupChats, getSuitableDiscussionChats, getGroupsInCommon, or getUserPrivacySettingRules
//@has_linked_chat True, if the channel has a discussion group, or the supergroup is the designated discussion group for a channel
//@has_location True, if the supergroup is connected to a location, i.e. the supergroup is a location-based supergroup
//@sign_messages True, if messages sent to the channel need to contain information about the sender. This field is only applicable to channels
@ -844,7 +844,7 @@ messageCalendar total_count:int32 days:vector<messageCalendarDay> = MessageCalen
//@description Describes a sponsored message @id Unique sponsored message identifier @sponsor_chat_id Chat identifier
//@link An internal link to be opened when the sponsored message is clicked; may be null. If null, the sponsor chat needs to be opened instead @content Content of the message
//@link An internal link to be opened when the sponsored message is clicked; may be null. If null, the sponsor chat needs to be opened instead @content Content of the message. Currently, can be only of the type messageText
sponsoredMessage id:int32 sponsor_chat_id:int53 link:InternalLinkType content:MessageContent = SponsoredMessage;
//@description Contains a list of sponsored messages @messages List of sponsored messages
@ -1095,7 +1095,7 @@ inlineKeyboardButtonTypeSwitchInline query:string in_current_chat:Bool = InlineK
//@description A button to buy something. This button must be in the first column and row of the keyboard and can be attached only to a message with content of the type messageInvoice
inlineKeyboardButtonTypeBuy = InlineKeyboardButtonType;
//@description A button to open a chat with a user @user_id User identifier
//@description A button with a user reference to be handled in the same way as textEntityTypeMentionName entities @user_id User identifier
inlineKeyboardButtonTypeUser user_id:int53 = InlineKeyboardButtonType;
@ -1334,7 +1334,7 @@ pageBlockMap location:location zoom:int32 width:int32 height:int32 caption:pageB
//@description Describes an instant view page for a web page
//@page_blocks Content of the web page
//@view_count Number of the instant view views; 0 if unknown
//@version Version of the instant view, currently can be 1 or 2
//@version Version of the instant view; currently, can be 1 or 2
//@is_rtl True, if the instant view must be shown from right to left
//@is_full True, if the instant view contains the full page. A network request might be needed to get the full web page instant view
//@feedback_link An internal link to be opened to leave feedback about the instant view
@ -1357,12 +1357,12 @@ webPageInstantView page_blocks:vector<PageBlock> view_count:int32 version:int32
//@author Author of the content
//@animation Preview of the content as an animation, if available; may be null
//@audio Preview of the content as an audio file, if available; may be null
//@document Preview of the content as a document, if available (currently only available for small PDF files and ZIP archives); may be null
//@document Preview of the content as a document, if available; may be null
//@sticker Preview of the content as a sticker for small WEBP files, if available; may be null
//@video Preview of the content as a video, if available; may be null
//@video_note Preview of the content as a video note, if available; may be null
//@voice_note Preview of the content as a voice note, if available; may be null
//@instant_view_version Version of instant view, available for the web page (currently can be 1 or 2), 0 if none
//@instant_view_version Version of instant view, available for the web page (currently, can be 1 or 2), 0 if none
webPage url:string display_url:string type:string site_name:string title:string description:formattedText photo:photo embed_url:string embed_type:string embed_width:int32 embed_height:int32 duration:int32 author:string animation:animation audio:audio document:document sticker:sticker video:video video_note:videoNote voice_note:voiceNote instant_view_version:int32 = WebPage;
@ -4222,10 +4222,10 @@ loadChats chat_list:ChatList limit:int32 = Ok;
//@chat_list The chat list in which to return chats; pass null to get chats from the main chat list @limit The maximum number of chats to be returned
getChats chat_list:ChatList limit:int32 = Chats;
//@description Searches a public chat by its username. Currently only private chats, supergroups and channels can be public. Returns the chat if found; otherwise an error is returned @username Username to be resolved
//@description Searches a public chat by its username. Currently, only private chats, supergroups and channels can be public. Returns the chat if found; otherwise an error is returned @username Username to be resolved
searchPublicChat username:string = Chat;
//@description Searches public chats by looking for specified query in their username and title. Currently only private chats, supergroups and channels can be public. Returns a meaningful number of results.
//@description Searches public chats by looking for specified query in their username and title. Currently, only private chats, supergroups and channels can be public. Returns a meaningful number of results.
//-Excludes private chats with contacts and chats from the chat list from the results @query Query to search for
searchPublicChats query:string = Chats;
@ -4425,7 +4425,7 @@ setChatDefaultMessageSender chat_id:int53 default_message_sender_id:MessageSende
//@input_message_content The content of the message to be sent
sendMessage chat_id:int53 message_thread_id:int53 reply_to_message_id:int53 options:messageSendOptions reply_markup:ReplyMarkup input_message_content:InputMessageContent = Message;
//@description Sends 2-10 messages grouped together into an album. Currently only audio, document, photo and video messages can be grouped into an album. Documents and audio files can be only grouped in an album with messages of the same type. Returns sent messages
//@description Sends 2-10 messages grouped together into an album. Currently, only audio, document, photo and video messages can be grouped into an album. Documents and audio files can be only grouped in an album with messages of the same type. Returns sent messages
//@chat_id Target chat
//@message_thread_id If not 0, a message thread identifier in which the messages will be sent
//@reply_to_message_id Identifier of a message to reply to or 0
@ -4854,7 +4854,7 @@ leaveChat chat_id:int53 = Ok;
//@chat_id Chat identifier @user_id Identifier of the user @forward_limit The number of earlier messages from the chat to be forwarded to the new member; up to 100. Ignored for supergroups and channels, or if the added user is a bot
addChatMember chat_id:int53 user_id:int53 forward_limit:int32 = Ok;
//@description Adds multiple new members to a chat. Currently this method is only available for supergroups and channels. This method can't be used to join a chat. Members can't be added to a channel if it has more than 200 members
//@description Adds multiple new members to a chat. Currently, this method is only available for supergroups and channels. This method can't be used to join a chat. Members can't be added to a channel if it has more than 200 members
//@chat_id Chat identifier @user_ids Identifiers of the users to be added to the chat. The maximum number of added users is 20 for supergroups and 100 for channels
addChatMembers chat_id:int53 user_ids:vector<int53> = Ok;
@ -5260,7 +5260,7 @@ getArchivedStickerSets is_masks:Bool offset_sticker_set_id:int64 limit:int32 = S
//@limit The maximum number of sticker sets to be returned; up to 100. For optimal performance, the number of returned sticker sets is chosen by TDLib and can be smaller than the specified limit, even if the end of the list has not been reached
getTrendingStickerSets offset:int32 limit:int32 = StickerSets;
//@description Returns a list of sticker sets attached to a file. Currently only photos and videos can have attached sticker sets @file_id File identifier
//@description Returns a list of sticker sets attached to a file. Currently, only photos and videos can have attached sticker sets @file_id File identifier
getAttachedStickerSets file_id:int32 = StickerSets;
//@description Returns information about a sticker set by its identifier @set_id Identifier of the sticker set
@ -5586,15 +5586,15 @@ deleteAccount reason:string = Ok;
//@description Removes a chat action bar without any other action @chat_id Chat identifier
removeChatActionBar chat_id:int53 = Ok;
//@description Reports a chat to the Telegram moderators. A chat can be reported only from the chat action bar, or if this is a private chat with a bot, a private chat with a user sharing their location, a supergroup, or a channel, since other chats can't be checked by moderators
//@description Reports a chat to the Telegram moderators. A chat can be reported only from the chat action bar, or if chat.can_be_reported
//@chat_id Chat identifier @message_ids Identifiers of reported messages, if any @reason The reason for reporting the chat @text Additional report details; 0-1024 characters
reportChat chat_id:int53 message_ids:vector<int53> reason:ChatReportReason text:string = Ok;
//@description Reports a chat photo to the Telegram moderators. A chat photo can be reported only if this is a private chat with a bot, a private chat with a user sharing their location, a supergroup, or a channel, since other chats can't be checked by moderators
//@description Reports a chat photo to the Telegram moderators. A chat photo can be reported only if chat.can_be_reported
//@chat_id Chat identifier @file_id Identifier of the photo to report. Only full photos from chatPhoto can be reported @reason The reason for reporting the chat photo @text Additional report details; 0-1024 characters
reportChatPhoto chat_id:int53 file_id:int32 reason:ChatReportReason text:string = Ok;
//@description Returns detailed statistics about a chat. Currently this method can be used only for supergroups and channels. Can be used only if supergroupFullInfo.can_get_statistics == true @chat_id Chat identifier @is_dark Pass true if a dark theme is used by the application
//@description Returns detailed statistics about a chat. Currently, this method can be used only for supergroups and channels. Can be used only if supergroupFullInfo.can_get_statistics == true @chat_id Chat identifier @is_dark Pass true if a dark theme is used by the application
getChatStatistics chat_id:int53 is_dark:Bool = ChatStatistics;
//@description Returns detailed statistics about a message. Can be used only if message.can_get_statistics == true @chat_id Chat identifier @message_id Message identifier @is_dark Pass true if a dark theme is used by the application

View File

@ -15676,9 +15676,9 @@ void ContactsManager::on_chat_update(telegram_api::channel &channel, const char
c->is_changed = true;
invalidate_channel_full(channel_id, !c->is_slow_mode_enabled);
}
if (c->is_verified != is_verified || c->sign_messages != sign_messages) {
// sign_messages isn't known for min-channels
if (c->is_verified != is_verified) {
c->is_verified = is_verified;
c->sign_messages = sign_messages;
c->is_changed = true;
}

View File

@ -883,10 +883,10 @@ unique_ptr<LinkManager::InternalLink> LinkManager::parse_tg_link_query(Slice que
}
}
} else if (path.size() == 1 && path[0] == "privatepost") {
// privatepost?channel=123456789&msg_id=12345&single&thread=<thread_id>&comment=<message_id>&t=<media_timestamp>
if (has_arg("channel") && has_arg("msg_id")) {
// privatepost?channel=123456789&post=12345&single&thread=<thread_id>&comment=<message_id>&t=<media_timestamp>
if (has_arg("channel") && has_arg("post")) {
return td::make_unique<InternalLinkMessage>(
PSTRING() << "tg:privatepost" << copy_arg("channel") << copy_arg("msg_id") << copy_arg("single")
PSTRING() << "tg:privatepost" << copy_arg("channel") << copy_arg("post") << copy_arg("single")
<< copy_arg("thread") << copy_arg("comment") << copy_arg("t"));
}
} else if (path.size() == 1 && path[0] == "bg") {
@ -941,10 +941,9 @@ unique_ptr<LinkManager::InternalLink> LinkManager::parse_t_me_link_query(Slice q
if (path.size() >= 3 && to_integer<int64>(path[1]) > 0 && to_integer<int64>(path[2]) > 0) {
// /c/123456789/12345?single&thread=<thread_id>&comment=<message_id>&t=<media_timestamp>
is_first_arg = false;
return td::make_unique<InternalLinkMessage>(PSTRING()
<< "tg:privatepost?channel=" << to_integer<int64>(path[1])
<< "&msg_id=" << to_integer<int64>(path[2]) << copy_arg("single")
<< copy_arg("thread") << copy_arg("comment") << copy_arg("t"));
return td::make_unique<InternalLinkMessage>(
PSTRING() << "tg:privatepost?channel=" << to_integer<int64>(path[1]) << "&post=" << to_integer<int64>(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()) {
@ -1296,7 +1295,7 @@ Result<MessageLinkInfo> LinkManager::get_message_link_info(Slice url) {
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
// privatepost?channel=123456789&post=12345&single&t=123&comment=12&thread=21
bool is_resolve = false;
if (begins_with(url, "resolve")) {
@ -1323,16 +1322,13 @@ Result<MessageLinkInfo> LinkManager::get_message_link_info(Slice url) {
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 == "post") {
message_id_slice = key_value.second;
}
if (key_value.first == "t") {
media_timestamp_slice = key_value.second;

View File

@ -448,7 +448,7 @@ class MessageChatSetTtl final : public MessageContent {
class MessageUnsupported final : public MessageContent {
public:
static constexpr int32 CURRENT_VERSION = 7;
static constexpr int32 CURRENT_VERSION = 8;
int32 version = CURRENT_VERSION;
MessageUnsupported() = default;

View File

@ -10920,22 +10920,28 @@ void MessagesManager::find_newer_messages(const Message *m, MessageId min_messag
}
void MessagesManager::find_unloadable_messages(const Dialog *d, int32 unload_before_date, const Message *m,
vector<MessageId> &message_ids, int32 &left_to_unload) const {
vector<MessageId> &message_ids,
bool &has_left_to_unload_messages) const {
if (m == nullptr) {
return;
}
find_unloadable_messages(d, unload_before_date, m->left.get(), message_ids, left_to_unload);
find_unloadable_messages(d, unload_before_date, m->left.get(), message_ids, has_left_to_unload_messages);
if (can_unload_message(d, m)) {
if (m->last_access_date <= unload_before_date) {
message_ids.push_back(m->message_id);
} else {
left_to_unload++;
has_left_to_unload_messages = true;
}
}
find_unloadable_messages(d, unload_before_date, m->right.get(), message_ids, left_to_unload);
if (has_left_to_unload_messages && m->date > unload_before_date) {
// we aren't interested in unloading too new messages
return;
}
find_unloadable_messages(d, unload_before_date, m->right.get(), message_ids, has_left_to_unload_messages);
}
void MessagesManager::delete_dialog_messages_by_sender(DialogId dialog_id, DialogId sender_dialog_id,
@ -10995,7 +11001,7 @@ void MessagesManager::delete_dialog_messages_by_sender(DialogId dialog_id, Dialo
}
vector<MessageId> message_ids;
find_messages(d->messages.get(), message_ids, [this, sender_dialog_id](const Message *m) {
find_messages(d->messages.get(), message_ids, [sender_dialog_id](const Message *m) {
return sender_dialog_id == MessagesManager::get_message_sender(m);
});
@ -11202,6 +11208,11 @@ int32 MessagesManager::get_unload_dialog_delay() const {
return narrow_cast<int32>(G()->shared_config().get_option_integer("message_unload_delay", default_unload_delay));
}
int32 MessagesManager::get_next_unload_dialog_delay() const {
auto delay = get_unload_dialog_delay();
return Random::fast(delay / 4, delay / 2);
}
void MessagesManager::unload_dialog(DialogId dialog_id) {
if (G()->close_flag()) {
return;
@ -11224,9 +11235,9 @@ void MessagesManager::unload_dialog(DialogId dialog_id) {
}
vector<MessageId> to_unload_message_ids;
int32 left_to_unload = 0;
bool has_left_to_unload_messages = false;
find_unloadable_messages(d, G()->unix_time_cached() - get_unload_dialog_delay() + 2, d->messages.get(),
to_unload_message_ids, left_to_unload);
to_unload_message_ids, has_left_to_unload_messages);
vector<int64> unloaded_message_ids;
for (auto message_id : to_unload_message_ids) {
@ -11244,9 +11255,9 @@ void MessagesManager::unload_dialog(DialogId dialog_id) {
make_tl_object<td_api::updateDeleteMessages>(dialog_id.get(), std::move(unloaded_message_ids), false, true));
}
if (left_to_unload > 0) {
LOG(DEBUG) << "Need to unload " << left_to_unload << " messages more in " << dialog_id;
pending_unload_dialog_timeout_.add_timeout_in(d->dialog_id.get(), get_unload_dialog_delay());
if (has_left_to_unload_messages) {
LOG(DEBUG) << "Need to unload more messages in " << dialog_id;
pending_unload_dialog_timeout_.add_timeout_in(d->dialog_id.get(), get_next_unload_dialog_delay());
} else {
d->has_unload_timeout = false;
}
@ -20274,8 +20285,8 @@ void MessagesManager::close_dialog(Dialog *d) {
if (is_message_unload_enabled()) {
CHECK(!d->has_unload_timeout);
pending_unload_dialog_timeout_.set_timeout_in(dialog_id.get(), get_next_unload_dialog_delay());
d->has_unload_timeout = true;
pending_unload_dialog_timeout_.set_timeout_in(dialog_id.get(), get_unload_dialog_delay());
}
for (auto &it : d->pending_viewed_live_locations) {
@ -24212,8 +24223,14 @@ void MessagesManager::get_dialog_send_message_as_dialog_ids(
} else {
add_sender(get_my_dialog_id());
}
for (auto channel_id : created_public_broadcasts_) {
add_sender(DialogId(channel_id));
auto sorted_channel_ids = transform(created_public_broadcasts_, [&](ChannelId channel_id) {
auto participant_count = td_->contacts_manager_->get_channel_participant_count(channel_id);
return std::make_pair(-participant_count, channel_id.get());
});
std::sort(sorted_channel_ids.begin(), sorted_channel_ids.end());
for (auto channel_id : sorted_channel_ids) {
add_sender(DialogId(ChannelId(channel_id.second)));
}
}
return promise.set_value(std::move(senders));
@ -24265,7 +24282,7 @@ void MessagesManager::set_dialog_default_send_message_as_dialog_id(DialogId dial
break;
}
if (!is_broadcast_channel(message_sender_dialog_id) ||
td_->contacts_manager_->get_channel_username(dialog_id.get_channel_id()).empty()) {
td_->contacts_manager_->get_channel_username(message_sender_dialog_id.get_channel_id()).empty()) {
return promise.set_error(Status::Error(400, "Message sender chat must be a public channel"));
}
break;
@ -33200,7 +33217,7 @@ MessagesManager::Message *MessagesManager::add_message_to_dialog(Dialog *d, uniq
if (!d->is_opened && d->messages != nullptr && is_message_unload_enabled() && !d->has_unload_timeout) {
LOG(INFO) << "Schedule unload of " << dialog_id;
pending_unload_dialog_timeout_.add_timeout_in(dialog_id.get(), get_unload_dialog_delay());
pending_unload_dialog_timeout_.add_timeout_in(dialog_id.get(), get_next_unload_dialog_delay());
d->has_unload_timeout = true;
}

View File

@ -2005,6 +2005,8 @@ class MessagesManager final : public Actor {
int32 get_unload_dialog_delay() const;
int32 get_next_unload_dialog_delay() const;
void unload_dialog(DialogId dialog_id);
void delete_all_dialog_messages(Dialog *d, bool remove_from_dialog_list, bool is_permanently_deleted);
@ -2061,7 +2063,7 @@ class MessagesManager final : public Actor {
static void find_newer_messages(const Message *m, MessageId min_message_id, vector<MessageId> &message_ids);
void find_unloadable_messages(const Dialog *d, int32 unload_before_date, const Message *m,
vector<MessageId> &message_ids, int32 &left_to_unload) const;
vector<MessageId> &message_ids, bool &has_left_to_unload_messages) const;
void on_pending_message_views_timeout(DialogId dialog_id);

View File

@ -1652,16 +1652,16 @@ void UpdatesManager::on_pending_updates(vector<tl_object_ptr<telegram_api::Updat
return promise.set_value(Unit());
}
}
}
if (date > 0 && updates.size() == 1 && updates[0] != nullptr &&
updates[0]->get_id() == telegram_api::updateReadHistoryOutbox::ID) {
auto update = static_cast<const telegram_api::updateReadHistoryOutbox *>(updates[0].get());
DialogId dialog_id(update->peer_);
if (dialog_id.get_type() == DialogType::User) {
auto user_id = dialog_id.get_user_id();
if (user_id.is_valid()) {
td_->contacts_manager_->on_update_user_local_was_online(user_id, date);
}
if (date > 0 && updates.size() == 1 && updates[0] != nullptr &&
updates[0]->get_id() == telegram_api::updateReadHistoryOutbox::ID) {
auto update = static_cast<const telegram_api::updateReadHistoryOutbox *>(updates[0].get());
DialogId dialog_id(update->peer_);
if (dialog_id.get_type() == DialogType::User) {
auto user_id = dialog_id.get_user_id();
if (user_id.is_valid()) {
td_->contacts_manager_->on_update_user_local_was_online(user_id, date);
}
}
}
@ -2029,6 +2029,14 @@ void UpdatesManager::add_pending_pts_update(tl_object_ptr<telegram_api::Update>
return;
}
// is_acceptable_update check was skipped for postponed pts updates
if (Slice(source) == "after get difference" && !is_acceptable_update(update.get())) {
LOG(INFO) << "Postpone again unacceptable pending update";
postpone_pts_update(std::move(update), new_pts, pts_count, receive_time, std::move(promise));
set_pts_gap_timeout(0.001);
return;
}
if (old_pts > new_pts - pts_count) {
LOG(WARNING) << "Have old_pts (= " << old_pts << ") + pts_count (= " << pts_count << ") > new_pts (= " << new_pts
<< "). Logged in " << G()->shared_config().get_option_integer("authorization_date") << ". Update from "

View File

@ -233,18 +233,18 @@ TEST(Link, parse_internal_link) {
unknown_deep_link("tg://privatepost?domain=username/12345&single"));
parse_internal_link("tg:privatepost?channel=username/12345&single",
unknown_deep_link("tg://privatepost?channel=username/12345&single"));
parse_internal_link("tg:privatepost?channel=username&msg_id=12345",
message("tg:privatepost?channel=username&msg_id=12345"));
parse_internal_link("tg:privatepost?channel=username&post=12345",
message("tg:privatepost?channel=username&post=12345"));
parse_internal_link("t.me/c/12345?single", nullptr);
parse_internal_link("t.me/c/1/c?single", nullptr);
parse_internal_link("t.me/c/c/1?single", nullptr);
parse_internal_link("t.me/c//1?single", nullptr);
parse_internal_link("t.me/c/12345/123", message("tg:privatepost?channel=12345&msg_id=123"));
parse_internal_link("t.me/c/12345/123?single", message("tg:privatepost?channel=12345&msg_id=123&single"));
parse_internal_link("t.me/c/12345/123/asd/asd////?single", message("tg:privatepost?channel=12345&msg_id=123&single"));
parse_internal_link("t.me/c/12345/123", message("tg:privatepost?channel=12345&post=123"));
parse_internal_link("t.me/c/12345/123?single", message("tg:privatepost?channel=12345&post=123&single"));
parse_internal_link("t.me/c/12345/123/asd/asd////?single", message("tg:privatepost?channel=12345&post=123&single"));
parse_internal_link("t.me/c/%312345/%3123?comment=456&t=789&single&thread=123%20%31",
message("tg:privatepost?channel=12345&msg_id=123&single&thread=123%201&comment=456&t=789"));
message("tg:privatepost?channel=12345&post=123&single&thread=123%201&comment=456&t=789"));
parse_internal_link("tg:bg?color=111111#asdasd", background("111111"));
parse_internal_link("tg:bg?color=11111%31", background("111111"));