// // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2023 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // #include "td/telegram/QuickReplyManager.h" #include "td/telegram/AccessRights.h" #include "td/telegram/AuthManager.h" #include "td/telegram/ChatManager.h" #include "td/telegram/Dependencies.h" #include "td/telegram/DialogManager.h" #include "td/telegram/FileReferenceManager.h" #include "td/telegram/files/FileManager.h" #include "td/telegram/files/FileType.h" #include "td/telegram/Global.h" #include "td/telegram/InlineQueriesManager.h" #include "td/telegram/logevent/LogEvent.h" #include "td/telegram/logevent/LogEventHelper.h" #include "td/telegram/MessageContent.h" #include "td/telegram/MessageContentType.h" #include "td/telegram/MessageCopyOptions.h" #include "td/telegram/MessageInputReplyTo.h" #include "td/telegram/MessageQuote.h" #include "td/telegram/MessageReplyHeader.h" #include "td/telegram/MessageSelfDestructType.h" #include "td/telegram/misc.h" #include "td/telegram/OptionManager.h" #include "td/telegram/ReplyMarkup.h" #include "td/telegram/ReplyMarkup.hpp" #include "td/telegram/ServerMessageId.h" #include "td/telegram/StoryFullId.h" #include "td/telegram/Td.h" #include "td/telegram/TdDb.h" #include "td/telegram/telegram_api.h" #include "td/telegram/UserManager.h" #include "td/telegram/Version.h" #include "td/utils/algorithm.h" #include "td/utils/buffer.h" #include "td/utils/format.h" #include "td/utils/HashTableUtils.h" #include "td/utils/logging.h" #include "td/utils/misc.h" #include "td/utils/Random.h" #include "td/utils/Time.h" #include "td/utils/tl_helpers.h" #include "td/utils/unicode.h" #include "td/utils/utf8.h" #include #include #include namespace td { class GetQuickRepliesQuery final : public Td::ResultHandler { Promise> promise_; public: explicit GetQuickRepliesQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(int64 hash) { send_query(G()->net_query_creator().create(telegram_api::messages_getQuickReplies(hash), {{"quick_reply"}})); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } auto ptr = result_ptr.move_as_ok(); LOG(DEBUG) << "Receive result for GetQuickRepliesQuery: " << to_string(ptr); promise_.set_value(std::move(ptr)); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; class EditQuickReplyShortcutQuery final : public Td::ResultHandler { Promise promise_; public: explicit EditQuickReplyShortcutQuery(Promise &&promise) : promise_(std::move(promise)) { } void send(QuickReplyShortcutId shortcut_id, const string &name) { send_query(G()->net_query_creator().create(telegram_api::messages_editQuickReplyShortcut(shortcut_id.get(), name), {{"quick_reply"}})); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } promise_.set_value(Unit()); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; class DeleteQuickReplyShortcutQuery final : public Td::ResultHandler { Promise promise_; public: explicit DeleteQuickReplyShortcutQuery(Promise &&promise) : promise_(std::move(promise)) { } void send(QuickReplyShortcutId shortcut_id) { send_query(G()->net_query_creator().create(telegram_api::messages_deleteQuickReplyShortcut(shortcut_id.get()), {{"quick_reply"}})); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } promise_.set_value(Unit()); } void on_error(Status status) final { td_->quick_reply_manager_->reload_quick_reply_shortcuts(); promise_.set_error(std::move(status)); } }; class ReorderQuickRepliesQuery final : public Td::ResultHandler { Promise promise_; public: explicit ReorderQuickRepliesQuery(Promise &&promise) : promise_(std::move(promise)) { } void send(vector shortcut_ids) { send_query( G()->net_query_creator().create(telegram_api::messages_reorderQuickReplies( QuickReplyShortcutId::get_input_quick_reply_shortcut_ids(shortcut_ids)), {{"quick_reply"}})); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } promise_.set_value(Unit()); } void on_error(Status status) final { td_->quick_reply_manager_->reload_quick_reply_shortcuts(); promise_.set_error(std::move(status)); } }; class GetQuickReplyMessagesQuery final : public Td::ResultHandler { Promise> promise_; public: explicit GetQuickReplyMessagesQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(QuickReplyShortcutId shortcut_id, const vector &message_ids, int64 hash) { int32 flags = 0; if (!message_ids.empty()) { flags |= telegram_api::messages_getQuickReplyMessages::ID_MASK; } CHECK(shortcut_id.is_server()); send_query(G()->net_query_creator().create( telegram_api::messages_getQuickReplyMessages(flags, shortcut_id.get(), MessageId::get_server_message_ids(message_ids), hash), {{"quick_reply"}})); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } auto ptr = result_ptr.move_as_ok(); LOG(DEBUG) << "Receive result for GetQuickReplyMessagesQuery: " << to_string(ptr); promise_.set_value(std::move(ptr)); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; class DeleteQuickReplyMessagesQuery final : public Td::ResultHandler { Promise promise_; QuickReplyShortcutId shortcut_id_; public: explicit DeleteQuickReplyMessagesQuery(Promise &&promise) : promise_(std::move(promise)) { } void send(QuickReplyShortcutId shortcut_id, const vector &message_ids) { shortcut_id_ = shortcut_id; CHECK(shortcut_id.is_server()); send_query(G()->net_query_creator().create(telegram_api::messages_deleteQuickReplyMessages( shortcut_id.get(), MessageId::get_server_message_ids(message_ids)), {{"quick_reply"}})); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } promise_.set_value(Unit()); } void on_error(Status status) final { td_->quick_reply_manager_->reload_quick_reply_messages(shortcut_id_, Promise()); promise_.set_error(std::move(status)); } }; class QuickReplyManager::SendQuickReplyMessageQuery final : public Td::ResultHandler { int64 random_id_; QuickReplyShortcutId shortcut_id_; public: void send(const QuickReplyMessage *m) { random_id_ = m->random_id; shortcut_id_ = m->shortcut_id; int32 flags = telegram_api::messages_sendMessage::QUICK_REPLY_SHORTCUT_MASK; if (m->disable_web_page_preview) { flags |= telegram_api::messages_sendMessage::NO_WEBPAGE_MASK; } if (m->invert_media) { flags |= telegram_api::messages_sendMessage::INVERT_MEDIA_MASK; } auto reply_to = MessageInputReplyTo(m->reply_to_message_id, DialogId(), MessageQuote()).get_input_reply_to(td_, MessageId()); if (reply_to != nullptr) { flags |= telegram_api::messages_sendMessage::REPLY_TO_MASK; } CHECK(m->edited_content == nullptr); const FormattedText *message_text = get_message_content_text(m->content.get()); CHECK(message_text != nullptr); auto entities = get_input_message_entities(td_->user_manager_.get(), message_text, "SendQuickReplyMessageQuery"); if (!entities.empty()) { flags |= telegram_api::messages_sendMessage::ENTITIES_MASK; } send_query(G()->net_query_creator().create( telegram_api::messages_sendMessage( flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, telegram_api::make_object(), std::move(reply_to), message_text->text, m->random_id, nullptr, std::move(entities), 0, nullptr, td_->quick_reply_manager_->get_input_quick_reply_shortcut(m->shortcut_id)), {{"me"}})); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } auto ptr = result_ptr.move_as_ok(); LOG(INFO) << "Receive result for SendQuickReplyMessageQuery for " << random_id_ << ": " << to_string(ptr); td_->quick_reply_manager_->process_send_quick_reply_updates(shortcut_id_, std::move(ptr), {random_id_}); } void on_error(Status status) final { if (G()->close_flag()) { // do not send error, message will be re-sent after restart return; } LOG(INFO) << "Receive error for SendQuickReplyMessageQuery: " << status; td_->quick_reply_manager_->on_failed_send_quick_reply_messages(shortcut_id_, {random_id_}, std::move(status)); } }; class QuickReplyManager::SendQuickReplyInlineMessageQuery final : public Td::ResultHandler { int64 random_id_; QuickReplyShortcutId shortcut_id_; public: void send(const QuickReplyMessage *m) { random_id_ = m->random_id; shortcut_id_ = m->shortcut_id; int32 flags = telegram_api::messages_sendInlineBotResult::QUICK_REPLY_SHORTCUT_MASK; if (m->hide_via_bot) { flags |= telegram_api::messages_sendInlineBotResult::HIDE_VIA_MASK; } auto reply_to = MessageInputReplyTo(m->reply_to_message_id, DialogId(), MessageQuote()).get_input_reply_to(td_, MessageId()); if (reply_to != nullptr) { flags |= telegram_api::messages_sendInlineBotResult::REPLY_TO_MASK; } send_query(G()->net_query_creator().create( telegram_api::messages_sendInlineBotResult( flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, telegram_api::make_object(), std::move(reply_to), m->random_id, m->inline_query_id, m->inline_result_id, 0, nullptr, td_->quick_reply_manager_->get_input_quick_reply_shortcut(m->shortcut_id)), {{"me"}})); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } auto ptr = result_ptr.move_as_ok(); LOG(INFO) << "Receive result for SendQuickReplyInlineMessageQuery for " << random_id_ << ": " << to_string(ptr); td_->quick_reply_manager_->process_send_quick_reply_updates(shortcut_id_, std::move(ptr), {random_id_}); } void on_error(Status status) final { if (G()->close_flag()) { // do not send error, message will be re-sent after restart return; } LOG(INFO) << "Receive error for SendQuickReplyInlineMessageQuery: " << status; td_->quick_reply_manager_->on_failed_send_quick_reply_messages(shortcut_id_, {random_id_}, std::move(status)); } }; class QuickReplyManager::SendQuickReplyMediaQuery final : public Td::ResultHandler { int64 random_id_; QuickReplyShortcutId shortcut_id_; FileId file_id_; FileId thumbnail_file_id_; string file_reference_; bool was_uploaded_ = false; bool was_thumbnail_uploaded_ = false; public: void send(FileId file_id, FileId thumbnail_file_id, const QuickReplyMessage *m, telegram_api::object_ptr &&input_media) { random_id_ = m->random_id; shortcut_id_ = m->shortcut_id; file_id_ = file_id; thumbnail_file_id_ = thumbnail_file_id; file_reference_ = FileManager::extract_file_reference(input_media); was_uploaded_ = FileManager::extract_was_uploaded(input_media); was_thumbnail_uploaded_ = FileManager::extract_was_thumbnail_uploaded(input_media); int32 flags = telegram_api::messages_sendMedia::QUICK_REPLY_SHORTCUT_MASK; if (m->invert_media) { flags |= telegram_api::messages_sendMedia::INVERT_MEDIA_MASK; } auto reply_to = MessageInputReplyTo(m->reply_to_message_id, DialogId(), MessageQuote()).get_input_reply_to(td_, MessageId()); if (reply_to != nullptr) { flags |= telegram_api::messages_sendMedia::REPLY_TO_MASK; } CHECK(m->edited_content == nullptr); vector> entities; const FormattedText *message_text = get_message_content_text(m->content.get()); if (message_text != nullptr) { entities = get_input_message_entities(td_->user_manager_.get(), message_text, "SendQuickReplyMediaQuery"); if (!entities.empty()) { flags |= telegram_api::messages_sendMedia::ENTITIES_MASK; } } send_query(G()->net_query_creator().create( telegram_api::messages_sendMedia( flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, telegram_api::make_object(), std::move(reply_to), std::move(input_media), message_text == nullptr ? string() : message_text->text, m->random_id, nullptr, std::move(entities), 0, nullptr, td_->quick_reply_manager_->get_input_quick_reply_shortcut(m->shortcut_id)), {{"me"}})); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } if (was_thumbnail_uploaded_) { CHECK(thumbnail_file_id_.is_valid()); // always delete partial remote location for the thumbnail, because it can't be reused anyway td_->file_manager_->delete_partial_remote_location(thumbnail_file_id_); } auto ptr = result_ptr.move_as_ok(); LOG(INFO) << "Receive result for SendQuickReplyMediaQuery for " << random_id_ << ": " << to_string(ptr); td_->quick_reply_manager_->process_send_quick_reply_updates(shortcut_id_, std::move(ptr), {random_id_}); } void on_error(Status status) final { if (G()->close_flag()) { // do not send error, message will be re-sent after restart return; } LOG(INFO) << "Receive error for SendQuickReplyMediaQuery: " << status; if (was_uploaded_) { if (was_thumbnail_uploaded_) { CHECK(thumbnail_file_id_.is_valid()); // always delete partial remote location for the thumbnail, because it can't be reused anyway td_->file_manager_->delete_partial_remote_location(thumbnail_file_id_); } CHECK(file_id_.is_valid()); auto bad_parts = FileManager::get_missing_file_parts(status); if (!bad_parts.empty()) { td_->quick_reply_manager_->on_send_message_file_parts_missing(shortcut_id_, random_id_, std::move(bad_parts)); return; } else { td_->file_manager_->delete_partial_remote_location_if_needed(file_id_, status); } } else if (FileReferenceManager::is_file_reference_error(status)) { if (file_id_.is_valid() && !was_uploaded_) { VLOG(file_references) << "Receive " << status << " for " << file_id_; td_->file_manager_->delete_file_reference(file_id_, file_reference_); td_->quick_reply_manager_->on_send_message_file_reference_error(shortcut_id_, random_id_); return; } else { LOG(ERROR) << "Receive file reference error, but file_id = " << file_id_ << ", was_uploaded = " << was_uploaded_; } } td_->quick_reply_manager_->on_failed_send_quick_reply_messages(shortcut_id_, {random_id_}, std::move(status)); } }; class QuickReplyManager::EditQuickReplyMessageQuery final : public Td::ResultHandler { QuickReplyShortcutId shortcut_id_; MessageId message_id_; int64 edit_generation_ = 0; FileId file_id_; FileId thumbnail_file_id_; string file_reference_; bool was_uploaded_ = false; bool was_thumbnail_uploaded_ = false; public: void send(FileId file_id, FileId thumbnail_file_id, const QuickReplyMessage *m, telegram_api::object_ptr &&input_media) { CHECK(m != nullptr); CHECK(m->edited_content != nullptr); CHECK(m->edit_generation > 0); shortcut_id_ = m->shortcut_id; message_id_ = m->message_id; edit_generation_ = m->edit_generation; file_id_ = file_id; thumbnail_file_id_ = thumbnail_file_id; file_reference_ = FileManager::extract_file_reference(input_media); was_uploaded_ = FileManager::extract_was_uploaded(input_media); was_thumbnail_uploaded_ = FileManager::extract_was_thumbnail_uploaded(input_media); auto *content = m->edited_content.get(); const auto *text = get_message_content_text(content); vector> entities; int32 flags = telegram_api::messages_editMessage::QUICK_REPLY_SHORTCUT_ID_MASK; if (text != nullptr) { entities = get_input_message_entities(td_->user_manager_.get(), text, "EditQuickReplyMessageQuery"); if (!entities.empty()) { flags |= telegram_api::messages_editMessage::ENTITIES_MASK; } flags |= telegram_api::messages_editMessage::MESSAGE_MASK; } if (m->edited_invert_media) { flags |= telegram_api::messages_editMessage::INVERT_MEDIA_MASK; } if (m->edited_disable_web_page_preview) { flags |= telegram_api::messages_editMessage::NO_WEBPAGE_MASK; } if (input_media != nullptr) { flags |= telegram_api::messages_editMessage::MEDIA_MASK; } CHECK(m->shortcut_id.is_server()); send_query(G()->net_query_creator().create( telegram_api::messages_editMessage( flags, false /*ignored*/, false /*ignored*/, telegram_api::make_object(), m->message_id.get_server_message_id().get(), text != nullptr ? text->text : string(), std::move(input_media), nullptr, std::move(entities), 0, m->shortcut_id.get()), {{"me"}})); } void on_result(BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(result_ptr.move_as_error()); } if (was_thumbnail_uploaded_) { CHECK(thumbnail_file_id_.is_valid()); // always delete partial remote location for the thumbnail, because it can't be reused anyway td_->file_manager_->delete_partial_remote_location(thumbnail_file_id_); } auto ptr = result_ptr.move_as_ok(); LOG(INFO) << "Receive result for EditQuickReplyMessageQuery: " << to_string(ptr); td_->quick_reply_manager_->on_edit_quick_reply_message(shortcut_id_, message_id_, edit_generation_, file_id_, was_uploaded_, std::move(ptr)); } void on_error(Status status) final { if (G()->close_flag()) { // do not send error, message will be re-edited after restart return; } if (!td_->auth_manager_->is_bot() && status.message() == "MESSAGE_NOT_MODIFIED") { if (was_thumbnail_uploaded_) { CHECK(thumbnail_file_id_.is_valid()); // always delete partial remote location for the thumbnail, because it can't be reused anyway td_->file_manager_->delete_partial_remote_location(thumbnail_file_id_); } return td_->quick_reply_manager_->on_edit_quick_reply_message(shortcut_id_, message_id_, edit_generation_, file_id_, was_uploaded_, nullptr); } td_->quick_reply_manager_->fail_edit_quick_reply_message(shortcut_id_, message_id_, edit_generation_, file_id_, thumbnail_file_id_, file_reference_, was_uploaded_, was_thumbnail_uploaded_, std::move(status)); } }; class QuickReplyManager::UploadMediaCallback final : public FileManager::UploadCallback { public: void on_upload_ok(FileId file_id, telegram_api::object_ptr input_file) final { send_closure_later(G()->quick_reply_manager(), &QuickReplyManager::on_upload_media, file_id, std::move(input_file)); } void on_upload_encrypted_ok(FileId file_id, telegram_api::object_ptr input_file) final { UNREACHABLE(); } void on_upload_secure_ok(FileId file_id, telegram_api::object_ptr input_file) final { UNREACHABLE(); } void on_upload_error(FileId file_id, Status error) final { send_closure_later(G()->quick_reply_manager(), &QuickReplyManager::on_upload_media_error, file_id, std::move(error)); } }; class QuickReplyManager::UploadThumbnailCallback final : public FileManager::UploadCallback { public: void on_upload_ok(FileId file_id, telegram_api::object_ptr input_file) final { send_closure_later(G()->quick_reply_manager(), &QuickReplyManager::on_upload_thumbnail, file_id, std::move(input_file)); } void on_upload_encrypted_ok(FileId file_id, telegram_api::object_ptr input_file) final { UNREACHABLE(); } void on_upload_secure_ok(FileId file_id, telegram_api::object_ptr input_file) final { UNREACHABLE(); } void on_upload_error(FileId file_id, Status error) final { send_closure_later(G()->quick_reply_manager(), &QuickReplyManager::on_upload_thumbnail, file_id, nullptr); } }; QuickReplyManager::QuickReplyMessage::~QuickReplyMessage() = default; template void QuickReplyManager::QuickReplyMessage::store(StorerT &storer) const { bool is_server = message_id.is_server(); bool has_edit_date = edit_date != 0; bool has_random_id = !is_server && random_id != 0; bool has_reply_to_message_id = reply_to_message_id != MessageId(); bool has_send_emoji = !is_server && !send_emoji.empty(); bool has_via_bot_user_id = via_bot_user_id != UserId(); bool has_legacy_layer = legacy_layer != 0; bool has_send_error_code = !is_server && send_error_code != 0; bool has_send_error_message = !is_server && !send_error_message.empty(); bool has_try_resend_at = !is_server && try_resend_at != 0; bool has_media_album_id = media_album_id != 0; bool has_reply_markup = reply_markup != nullptr; bool has_inline_query_id = inline_query_id != 0; bool has_inline_result_id = !inline_result_id.empty(); bool has_edited_content = edited_content != nullptr; BEGIN_STORE_FLAGS(); STORE_FLAG(has_edit_date); STORE_FLAG(has_random_id); STORE_FLAG(has_reply_to_message_id); STORE_FLAG(has_send_emoji); STORE_FLAG(has_via_bot_user_id); STORE_FLAG(is_failed_to_send); STORE_FLAG(disable_notification); STORE_FLAG(invert_media); STORE_FLAG(from_background); STORE_FLAG(disable_web_page_preview); STORE_FLAG(hide_via_bot); STORE_FLAG(has_legacy_layer); STORE_FLAG(has_send_error_code); STORE_FLAG(has_send_error_message); STORE_FLAG(has_try_resend_at); STORE_FLAG(has_media_album_id); STORE_FLAG(has_reply_markup); STORE_FLAG(has_inline_query_id); STORE_FLAG(has_inline_result_id); STORE_FLAG(has_edited_content); STORE_FLAG(edited_invert_media); STORE_FLAG(edited_disable_web_page_preview); END_STORE_FLAGS(); td::store(message_id, storer); td::store(shortcut_id, storer); if (has_edit_date) { td::store(edit_date, storer); } if (has_random_id) { td::store(random_id, storer); } if (has_reply_to_message_id) { td::store(reply_to_message_id, storer); } if (has_send_emoji) { td::store(send_emoji, storer); } if (has_via_bot_user_id) { td::store(via_bot_user_id, storer); } if (has_legacy_layer) { td::store(legacy_layer, storer); } if (has_send_error_code) { td::store(send_error_code, storer); } if (has_send_error_message) { td::store(send_error_message, storer); } if (has_try_resend_at) { td::store_time(try_resend_at, storer); } if (has_media_album_id) { td::store(media_album_id, storer); } store_message_content(content.get(), storer); if (has_reply_markup) { td::store(reply_markup, storer); } if (has_inline_query_id) { td::store(inline_query_id, storer); } if (has_inline_result_id) { td::store(inline_result_id, storer); } if (has_edited_content) { store_message_content(edited_content.get(), storer); } } template void QuickReplyManager::QuickReplyMessage::parse(ParserT &parser) { bool has_edit_date; bool has_random_id; bool has_reply_to_message_id; bool has_send_emoji; bool has_via_bot_user_id; bool has_legacy_layer; bool has_send_error_code; bool has_send_error_message; bool has_try_resend_at; bool has_media_album_id; bool has_reply_markup; bool has_inline_query_id; bool has_inline_result_id; bool has_edited_content; BEGIN_PARSE_FLAGS(); PARSE_FLAG(has_edit_date); PARSE_FLAG(has_random_id); PARSE_FLAG(has_reply_to_message_id); PARSE_FLAG(has_send_emoji); PARSE_FLAG(has_via_bot_user_id); PARSE_FLAG(is_failed_to_send); PARSE_FLAG(disable_notification); PARSE_FLAG(invert_media); PARSE_FLAG(from_background); PARSE_FLAG(disable_web_page_preview); PARSE_FLAG(hide_via_bot); PARSE_FLAG(has_legacy_layer); PARSE_FLAG(has_send_error_code); PARSE_FLAG(has_send_error_message); PARSE_FLAG(has_try_resend_at); PARSE_FLAG(has_media_album_id); PARSE_FLAG(has_reply_markup); PARSE_FLAG(has_inline_query_id); PARSE_FLAG(has_inline_result_id); PARSE_FLAG(has_edited_content); PARSE_FLAG(edited_invert_media); PARSE_FLAG(edited_disable_web_page_preview); END_PARSE_FLAGS(); td::parse(message_id, parser); td::parse(shortcut_id, parser); if (has_edit_date) { td::parse(edit_date, parser); } if (has_random_id) { td::parse(random_id, parser); } if (has_reply_to_message_id) { td::parse(reply_to_message_id, parser); } if (has_send_emoji) { td::parse(send_emoji, parser); } if (has_via_bot_user_id) { td::parse(via_bot_user_id, parser); } if (has_legacy_layer) { td::parse(legacy_layer, parser); } if (has_send_error_code) { td::parse(send_error_code, parser); } if (has_send_error_message) { td::parse(send_error_message, parser); } if (has_try_resend_at) { td::parse_time(try_resend_at, parser); } if (has_media_album_id) { td::parse(media_album_id, parser); } parse_message_content(content, parser); if (has_reply_markup) { td::parse(reply_markup, parser); } if (has_inline_query_id) { td::parse(inline_query_id, parser); } if (has_inline_result_id) { td::parse(inline_result_id, parser); } if (has_edited_content) { parse_message_content(edited_content, parser); } } QuickReplyManager::Shortcut::~Shortcut() = default; template void QuickReplyManager::Shortcut::store(StorerT &storer) const { int32 server_total_count = 0; int32 local_total_count = 0; for (const auto &message : messages_) { if (message->message_id.is_server()) { server_total_count++; } else { local_total_count++; } } CHECK(server_total_count <= server_total_count_); CHECK(local_total_count <= local_total_count_); bool has_server_total_count = server_total_count != 0; bool has_local_total_count = local_total_count != 0; BEGIN_STORE_FLAGS(); STORE_FLAG(has_server_total_count); STORE_FLAG(has_local_total_count); END_STORE_FLAGS(); td::store(name_, storer); td::store(shortcut_id_, storer); if (has_server_total_count) { td::store(server_total_count, storer); } if (has_local_total_count) { td::store(local_total_count, storer); } for (const auto &message : messages_) { td::store(message, storer); } } template void QuickReplyManager::Shortcut::parse(ParserT &parser) { bool has_server_total_count; bool has_local_total_count; BEGIN_PARSE_FLAGS(); PARSE_FLAG(has_server_total_count); PARSE_FLAG(has_local_total_count); END_PARSE_FLAGS(); td::parse(name_, parser); td::parse(shortcut_id_, parser); if (has_server_total_count) { td::parse(server_total_count_, parser); } if (has_local_total_count) { td::parse(local_total_count_, parser); } if (server_total_count_ < 0 || local_total_count_ < 0) { return parser.set_error("Wrong message count"); } auto size = static_cast(server_total_count_) + static_cast(local_total_count_); if (parser.get_left_len() < size) { return parser.set_error("Wrong message count"); } messages_ = vector>(size); for (auto &message : messages_) { td::parse(message, parser); } } template void QuickReplyManager::Shortcuts::store(StorerT &storer) const { BEGIN_STORE_FLAGS(); END_STORE_FLAGS(); td::store(shortcuts_, storer); } template void QuickReplyManager::Shortcuts::parse(ParserT &parser) { BEGIN_PARSE_FLAGS(); END_PARSE_FLAGS(); td::parse(shortcuts_, parser); } QuickReplyManager::QuickReplyManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) { upload_media_callback_ = std::make_shared(); upload_thumbnail_callback_ = std::make_shared(); } void QuickReplyManager::tear_down() { parent_.reset(); } bool QuickReplyManager::is_shortcut_name_letter(uint32 code) { auto category = get_unicode_simple_category(code); if (code == '_' || code == 0x200c || code == 0xb7 || (0xd80 <= code && code <= 0xdff)) { return true; } switch (category) { case UnicodeSimpleCategory::DecimalNumber: case UnicodeSimpleCategory::Letter: return true; default: return false; } } Status QuickReplyManager::check_shortcut_name(CSlice name) { if (!check_utf8(name)) { return Status::Error("Strings must be encoded in UTF-8"); } int32 length = 0; auto *ptr = name.ubegin(); while (ptr != name.uend()) { uint32 code; ptr = next_utf8_unsafe(ptr, &code); if (!is_shortcut_name_letter(code)) { return Status::Error("A letter is not allowed"); } length++; } if (length == 0) { return Status::Error("Name must be non-empty"); } if (length > 32) { return Status::Error("Name is too long"); } return Status::OK(); } unique_ptr QuickReplyManager::create_message( telegram_api::object_ptr message_ptr, const char *source) const { LOG(DEBUG) << "Receive from " << source << " " << to_string(message_ptr); CHECK(message_ptr != nullptr); switch (message_ptr->get_id()) { case telegram_api::messageEmpty::ID: break; case telegram_api::message::ID: { auto message_id = MessageId::get_message_id(message_ptr, false); auto message = move_tl_object_as(message_ptr); auto shortcut_id = QuickReplyShortcutId(message->quick_reply_shortcut_id_); if (!shortcut_id.is_server()) { LOG(ERROR) << "Receive invalid quick reply " << shortcut_id << " from " << source; break; } if (deleted_message_full_ids_.count({shortcut_id, message_id})) { // a previously deleted message break; } auto my_dialog_id = td_->dialog_manager_->get_my_dialog_id(); if (DialogId(message->peer_id_) != my_dialog_id || message->from_id_ != nullptr || message->fwd_from_ != nullptr || message->views_ != 0 || message->forwards_ != 0 || message->replies_ != nullptr || message->reactions_ != nullptr || message->ttl_period_ != 0 || !message->out_ || message->post_ || message->from_scheduled_ || message->pinned_ || message->noforwards_ || message->mentioned_ || !message->restriction_reason_.empty() || !message->post_author_.empty() || message->from_boosts_applied_ != 0) { LOG(ERROR) << "Receive an invalid quick reply from " << source << ": " << to_string(message); } if (message->saved_peer_id_ != nullptr) { LOG(DEBUG) << "Receive unneeded Saved Messages topic in quick reply " << message_id << " from " << source; } UserId via_bot_user_id; if (message->flags_ & telegram_api::message::VIA_BOT_ID_MASK) { via_bot_user_id = UserId(message->via_bot_id_); if (!via_bot_user_id.is_valid()) { LOG(ERROR) << "Receive invalid " << via_bot_user_id << " from " << source; via_bot_user_id = UserId(); } } auto media_album_id = message->grouped_id_; MessageSelfDestructType ttl; bool disable_web_page_preview = false; auto content = get_message_content( td_, get_message_text(td_->user_manager_.get(), std::move(message->message_), std::move(message->entities_), true, td_->auth_manager_->is_bot(), 0, media_album_id != 0, source), std::move(message->media_), my_dialog_id, message->date_, true, via_bot_user_id, &ttl, &disable_web_page_preview, source); auto reply_header = MessageReplyHeader(td_, std::move(message->reply_to_), my_dialog_id, message_id, -1, false); if (reply_header.story_full_id_ != StoryFullId()) { LOG(ERROR) << "Receive reply to " << reply_header.story_full_id_; reply_header.story_full_id_ = {}; } if (reply_header.replied_message_info_.is_external() || reply_header.replied_message_info_.get_reply_message_full_id(DialogId(), true).get_dialog_id() != DialogId()) { LOG(ERROR) << "Receive reply to " << reply_header.replied_message_info_; reply_header.replied_message_info_ = {}; } auto reply_to_message_id = reply_header.replied_message_info_.get_same_chat_reply_to_message_id(true); if (!ttl.is_empty()) { LOG(ERROR) << "Wrong " << ttl << " received in " << message_id << " from " << source; break; } auto content_type = content->get_type(); if (is_service_message_content(content_type) || content_type == MessageContentType::LiveLocation || is_expired_message_content(content_type) || content_type == MessageContentType::Poll) { LOG(ERROR) << "Receive " << content_type << " from " << source; break; } auto result = make_unique(); result->shortcut_id = shortcut_id; result->message_id = message_id; result->edit_date = max(message->edit_date_, 0); result->disable_web_page_preview = disable_web_page_preview; result->reply_to_message_id = reply_to_message_id; result->via_bot_user_id = via_bot_user_id; result->disable_notification = message->silent_; result->legacy_layer = (message->legacy_ ? MTPROTO_LAYER : 0); result->invert_media = message->invert_media_; result->content = std::move(content); result->reply_markup = get_reply_markup(std::move(message->reply_markup_), td_->auth_manager_->is_bot(), true, false); if (media_album_id != 0) { if (!is_allowed_media_group_content(content_type)) { if (content_type != MessageContentType::Unsupported) { LOG(ERROR) << "Receive media group identifier " << media_album_id << " in " << message_id << " with content " << oneline(to_string(get_quick_reply_message_message_content_object(result.get()))); } } else { result->media_album_id = media_album_id; } } Dependencies dependencies; add_quick_reply_message_dependencies(dependencies, result.get()); for (auto dependent_dialog_id : dependencies.get_dialog_ids()) { td_->dialog_manager_->force_create_dialog(dependent_dialog_id, source, true); } return result; } case telegram_api::messageService::ID: LOG(ERROR) << "Receive " << to_string(message_ptr); break; default: UNREACHABLE(); break; } return nullptr; } void QuickReplyManager::add_quick_reply_message_dependencies(Dependencies &dependencies, const QuickReplyMessage *m) const { auto is_bot = td_->auth_manager_->is_bot(); dependencies.add(m->via_bot_user_id); add_message_content_dependencies(dependencies, m->content.get(), is_bot); if (m->edited_content != nullptr) { add_message_content_dependencies(dependencies, m->edited_content.get(), is_bot); } add_reply_markup_dependencies(dependencies, m->reply_markup.get()); } bool QuickReplyManager::can_edit_quick_reply_message(const QuickReplyMessage *m) const { return m->message_id.is_server() && !m->via_bot_user_id.is_valid() && is_editable_message_content(m->content->get_type()) && m->content->get_type() != MessageContentType::Game; } bool QuickReplyManager::can_resend_quick_reply_message(const QuickReplyMessage *m) const { if (m->send_error_code != 429) { return false; } return true; } td_api::object_ptr QuickReplyManager::get_message_sending_state_object( const QuickReplyMessage *m) const { CHECK(m != nullptr); if (m->message_id.is_yet_unsent()) { return td_api::make_object(0); } if (m->is_failed_to_send) { auto can_retry = can_resend_quick_reply_message(m); auto error_code = m->send_error_code > 0 ? m->send_error_code : 400; auto need_another_reply_quote = can_retry && error_code == 400 && m->send_error_message == CSlice("QUOTE_TEXT_INVALID"); return td_api::make_object( td_api::make_object(error_code, m->send_error_message), can_retry, false, need_another_reply_quote, false, max(m->try_resend_at - Time::now(), 0.0)); } return nullptr; } td_api::object_ptr QuickReplyManager::get_quick_reply_message_message_content_object( const QuickReplyMessage *m) const { if (m->edited_content != nullptr) { return get_message_content_object(m->edited_content.get(), td_, DialogId(), 0, false, true, -1, m->edited_invert_media, m->edited_disable_web_page_preview); } return get_message_content_object(m->content.get(), td_, DialogId(), 0, false, true, -1, m->invert_media, m->disable_web_page_preview); } td_api::object_ptr QuickReplyManager::get_quick_reply_message_object( const QuickReplyMessage *m, const char *source) const { CHECK(m != nullptr); auto can_be_edited = can_edit_quick_reply_message(m); return td_api::make_object( m->message_id.get(), get_message_sending_state_object(m), can_be_edited, m->reply_to_message_id.get(), td_->user_manager_->get_user_id_object(m->via_bot_user_id, "via_bot_user_id"), m->media_album_id, get_quick_reply_message_message_content_object(m), get_reply_markup_object(td_->user_manager_.get(), m->reply_markup)); } int32 QuickReplyManager::get_shortcut_message_count(const Shortcut *s) { return s->server_total_count_ + s->local_total_count_; } bool QuickReplyManager::have_all_shortcut_messages(const Shortcut *s) { return static_cast(s->messages_.size()) == get_shortcut_message_count(s); } td_api::object_ptr QuickReplyManager::get_quick_reply_shortcut_object( const Shortcut *s, const char *source) const { CHECK(s != nullptr); CHECK(!s->messages_.empty()); return td_api::make_object(s->shortcut_id_.get(), s->name_, get_quick_reply_message_object(s->messages_[0].get(), source), get_shortcut_message_count(s)); } void QuickReplyManager::get_quick_reply_shortcuts(Promise &&promise) { load_quick_reply_shortcuts(); if (shortcuts_.are_inited_) { return promise.set_value(Unit()); } shortcuts_.load_queries_.push_back(std::move(promise)); if (shortcuts_.load_queries_.size() != 1) { return; } reload_quick_reply_shortcuts(); } void QuickReplyManager::reload_quick_reply_shortcuts() { if (td_->auth_manager_->is_bot()) { return; } load_quick_reply_shortcuts(); auto promise = PromiseCreator::lambda( [actor_id = actor_id(this)](Result> r_shortcuts) { send_closure(actor_id, &QuickReplyManager::on_reload_quick_reply_shortcuts, std::move(r_shortcuts)); }); td_->create_handler(std::move(promise))->send(get_shortcuts_hash()); } void QuickReplyManager::on_reload_quick_reply_shortcuts( Result> r_shortcuts) { G()->ignore_result_if_closing(r_shortcuts); if (r_shortcuts.is_error()) { return on_load_quick_reply_fail(r_shortcuts.move_as_error()); } auto shortcuts_ptr = r_shortcuts.move_as_ok(); switch (shortcuts_ptr->get_id()) { case telegram_api::messages_quickRepliesNotModified::ID: if (!shortcuts_.are_inited_) { shortcuts_.are_inited_ = true; save_quick_reply_shortcuts(); send_update_quick_reply_shortcuts(); } break; case telegram_api::messages_quickReplies::ID: { auto shortcuts = telegram_api::move_object_as(shortcuts_ptr); td_->user_manager_->on_get_users(std::move(shortcuts->users_), "messages.quickReplies"); td_->chat_manager_->on_get_chats(std::move(shortcuts->chats_), "messages.quickReplies"); FlatHashMap, MessageIdHash> message_id_to_message; for (auto &message : shortcuts->messages_) { auto message_id = MessageId::get_message_id(message, false); if (!message_id.is_valid()) { continue; } message_id_to_message[message_id] = std::move(message); } FlatHashSet old_shortcut_ids; for (auto &shortcut : shortcuts_.shortcuts_) { CHECK(shortcut->shortcut_id_.is_valid()); old_shortcut_ids.insert(shortcut->shortcut_id_); } FlatHashSet added_shortcut_ids; FlatHashSet added_shortcut_names; vector> new_shortcuts; vector changed_shortcut_ids; vector changed_message_shortcut_ids; vector deleted_shortcut_ids; for (auto &quick_reply : shortcuts->quick_replies_) { auto shortcut_id = QuickReplyShortcutId(quick_reply->shortcut_id_); if (!shortcut_id.is_server() || quick_reply->shortcut_.empty() || quick_reply->count_ <= 0 || quick_reply->top_message_ <= 0) { LOG(ERROR) << "Receive " << to_string(quick_reply); continue; } if (added_shortcut_ids.count(shortcut_id) || added_shortcut_names.count(quick_reply->shortcut_)) { LOG(ERROR) << "Receive duplicate " << to_string(quick_reply); continue; } if (deleted_shortcut_ids_.count(shortcut_id)) { continue; } added_shortcut_ids.insert(shortcut_id); added_shortcut_names.insert(quick_reply->shortcut_); MessageId first_message_id(ServerMessageId(quick_reply->top_message_)); auto it = message_id_to_message.find(first_message_id); if (it == message_id_to_message.end()) { LOG(ERROR) << "Can't find last " << first_message_id << " in " << shortcut_id; continue; } auto message = create_message(std::move(it->second), "on_reload_quick_reply_shortcuts"); message_id_to_message.erase(it); if (message == nullptr) { continue; } if (message->shortcut_id != shortcut_id) { LOG(ERROR) << "Receive message from " << message->shortcut_id << " instead of " << shortcut_id; continue; } auto shortcut = td::make_unique(); shortcut->name_ = std::move(quick_reply->shortcut_); shortcut->shortcut_id_ = shortcut_id; shortcut->server_total_count_ = quick_reply->count_; shortcut->messages_.push_back(std::move(message)); auto old_shortcut = get_shortcut(shortcut_id); if (old_shortcut == nullptr) { changed_shortcut_ids.push_back(shortcut_id); changed_message_shortcut_ids.push_back(shortcut_id); change_message_files({shortcut_id, first_message_id}, shortcut->messages_[0].get(), {}); } else { bool is_shortcut_changed = false; bool are_messages_changed = false; update_shortcut_from(shortcut.get(), old_shortcut, true, &is_shortcut_changed, &are_messages_changed); if (are_messages_changed) { changed_message_shortcut_ids.push_back(shortcut_id); } if (is_shortcut_changed) { changed_shortcut_ids.push_back(shortcut_id); } old_shortcut_ids.erase(shortcut_id); } new_shortcuts.push_back(std::move(shortcut)); } for (auto shortcut_id : old_shortcut_ids) { auto old_shortcut = get_shortcut(shortcut_id); CHECK(old_shortcut != nullptr); auto is_changed = td::remove_if(old_shortcut->messages_, [&](const unique_ptr &message) { if (message->message_id.is_server()) { delete_message_files(shortcut_id, message.get()); return true; } return false; }); if (old_shortcut->messages_.empty()) { CHECK(is_changed); send_update_quick_reply_shortcut_deleted(old_shortcut); } else { // some local messages left if (added_shortcut_names.count(old_shortcut->name_)) { LOG(INFO) << "Local shortcut " << old_shortcut->name_ << " has been created server-side"; for (auto &shortcut : new_shortcuts) { if (shortcut->name_ == old_shortcut->name_) { LOG(INFO) << "Move local messages from " << old_shortcut->shortcut_id_ << " to " << shortcut->shortcut_id_; CHECK(shortcut->local_total_count_ == 0); shortcut->local_total_count_ = static_cast(old_shortcut->messages_.size()); for (auto &message : old_shortcut->messages_) { CHECK(message->shortcut_id == shortcut_id); message->shortcut_id = shortcut->shortcut_id_; } append(shortcut->messages_, std::move(old_shortcut->messages_)); sort_quick_reply_messages(shortcut->messages_); send_update_quick_reply_shortcut_deleted(old_shortcut); changed_shortcut_ids.push_back(shortcut->shortcut_id_); changed_message_shortcut_ids.push_back(shortcut->shortcut_id_); break; } } continue; } LOG(INFO) << "Keep local shortcut " << old_shortcut->name_; auto shortcut = td::make_unique(); shortcut->name_ = std::move(old_shortcut->name_); shortcut->shortcut_id_ = old_shortcut->shortcut_id_; shortcut->server_total_count_ = 0; shortcut->local_total_count_ = static_cast(old_shortcut->messages_.size()); shortcut->messages_ = std::move(old_shortcut->messages_); if (is_changed) { changed_shortcut_ids.push_back(shortcut->shortcut_id_); changed_message_shortcut_ids.push_back(shortcut->shortcut_id_); } new_shortcuts.push_back(std::move(shortcut)); } } bool is_list_changed = is_shortcut_list_changed(new_shortcuts); shortcuts_.shortcuts_ = std::move(new_shortcuts); shortcuts_.are_inited_ = true; save_quick_reply_shortcuts(); for (auto shortcut_id : changed_shortcut_ids) { send_update_quick_reply_shortcut(get_shortcut(shortcut_id), "on_reload_quick_reply_shortcuts"); } for (auto shortcut_id : changed_message_shortcut_ids) { const auto *s = get_shortcut(shortcut_id); send_update_quick_reply_shortcut_messages(s, "on_reload_quick_reply_shortcuts"); } if (is_list_changed) { send_update_quick_reply_shortcuts(); } break; } default: UNREACHABLE(); } on_load_quick_reply_success(); } bool QuickReplyManager::is_shortcut_list_changed(const vector> &new_shortcuts) const { if (!shortcuts_.are_inited_ || shortcuts_.shortcuts_.size() != new_shortcuts.size()) { return true; } for (size_t i = 0; i < new_shortcuts.size(); i++) { if (shortcuts_.shortcuts_[i]->shortcut_id_ != new_shortcuts[i]->shortcut_id_) { return true; } } return false; } void QuickReplyManager::on_load_quick_reply_success() { for (auto &shortcut : shortcuts_.shortcuts_) { reload_quick_reply_messages(shortcut->shortcut_id_, Auto()); } set_promises(shortcuts_.load_queries_); } void QuickReplyManager::on_load_quick_reply_fail(Status error) { fail_promises(shortcuts_.load_queries_, std::move(error)); } int64 QuickReplyManager::get_shortcuts_hash() const { vector numbers; for (auto &shortcut : shortcuts_.shortcuts_) { for (auto &message : shortcut->messages_) { if (message->message_id.is_server()) { CHECK(shortcut->shortcut_id_.is_server()); numbers.push_back(shortcut->shortcut_id_.get()); numbers.push_back(get_md5_string_hash(shortcut->name_)); numbers.push_back(message->message_id.get_server_message_id().get()); numbers.push_back(message->edit_date); break; } } } return get_vector_hash(numbers); } void QuickReplyManager::set_quick_reply_shortcut_name(QuickReplyShortcutId shortcut_id, const string &name, Promise &&promise) { load_quick_reply_shortcuts(); const auto *shortcut = get_shortcut(shortcut_id); if (shortcut == nullptr) { return promise.set_error(Status::Error(400, "Shortcut not found")); } if (check_shortcut_name(name).is_error()) { return promise.set_error(Status::Error(400, "Shortcut name is invalid")); } if (!shortcut_id.is_server()) { return promise.set_error(Status::Error(400, "Shortcut isn't created yet")); } auto query_promise = PromiseCreator::lambda( [actor_id = actor_id(this), shortcut_id, name, promise = std::move(promise)](Result &&result) mutable { if (result.is_ok()) { send_closure(actor_id, &QuickReplyManager::on_set_quick_reply_shortcut_name, shortcut_id, name, std::move(promise)); } else { promise.set_error(result.move_as_error()); } }); set_quick_reply_shortcut_name_on_server(shortcut_id, name, std::move(query_promise)); } void QuickReplyManager::set_quick_reply_shortcut_name_on_server(QuickReplyShortcutId shortcut_id, const string &name, Promise &&promise) { CHECK(shortcut_id.is_server()); td_->create_handler(std::move(promise))->send(shortcut_id, name); } void QuickReplyManager::on_set_quick_reply_shortcut_name(QuickReplyShortcutId shortcut_id, const string &name, Promise &&promise) { auto *shortcut = get_shortcut(shortcut_id); if (shortcut == nullptr || shortcut->name_ == name) { return promise.set_value(Unit()); } shortcut->name_ = name; send_update_quick_reply_shortcut(shortcut, "on_set_quick_reply_shortcut_name"); save_quick_reply_shortcuts(); promise.set_value(Unit()); } void QuickReplyManager::delete_quick_reply_shortcut(QuickReplyShortcutId shortcut_id, Promise &&promise) { load_quick_reply_shortcuts(); auto it = get_shortcut_it(shortcut_id); if (it == shortcuts_.shortcuts_.end()) { return promise.set_error(Status::Error(400, "Shortcut not found")); } send_update_quick_reply_shortcut_deleted(it->get()); shortcuts_.shortcuts_.erase(it); save_quick_reply_shortcuts(); send_update_quick_reply_shortcuts(); if (!shortcut_id.is_server()) { return promise.set_value(Unit()); } delete_quick_reply_shortcut_from_server(shortcut_id, std::move(promise)); } void QuickReplyManager::delete_quick_reply_shortcut_from_server(QuickReplyShortcutId shortcut_id, Promise &&promise) { CHECK(shortcut_id.is_server()); deleted_shortcut_ids_.insert(shortcut_id); td_->create_handler(std::move(promise))->send(shortcut_id); } void QuickReplyManager::reorder_quick_reply_shortcuts(const vector &shortcut_ids, Promise &&promise) { load_quick_reply_shortcuts(); FlatHashSet unique_shortcut_ids; for (const auto &shortcut_id : shortcut_ids) { if (get_shortcut(shortcut_id) == nullptr) { return promise.set_error(Status::Error(400, "Shortcut not found")); } CHECK(shortcut_id.is_valid()); unique_shortcut_ids.insert(shortcut_id); } if (unique_shortcut_ids.size() != shortcut_ids.size()) { return promise.set_error(Status::Error(400, "Duplicate shortcut identifiers specified")); } if (!shortcuts_.are_inited_) { return promise.set_value(Unit()); } auto old_shortcut_ids = get_shortcut_ids(); auto old_server_shortcut_ids = get_server_shortcut_ids(); vector> shortcuts; for (const auto &shortcut_id : shortcut_ids) { auto it = get_shortcut_it(shortcut_id); CHECK(it != shortcuts_.shortcuts_.end() && *it != nullptr); shortcuts.push_back(std::move(*it)); } for (auto &shortcut : shortcuts_.shortcuts_) { if (shortcut != nullptr) { CHECK(unique_shortcut_ids.count(shortcut->shortcut_id_) == 0); shortcuts.push_back(std::move(shortcut)); } } shortcuts_.shortcuts_ = std::move(shortcuts); if (old_shortcut_ids == get_shortcut_ids()) { return promise.set_value(Unit()); } save_quick_reply_shortcuts(); send_update_quick_reply_shortcuts(); auto new_server_shortcut_ids = get_server_shortcut_ids(); if (new_server_shortcut_ids == old_server_shortcut_ids) { return promise.set_value(Unit()); } reorder_quick_reply_shortcuts_on_server(new_server_shortcut_ids, std::move(promise)); } void QuickReplyManager::reorder_quick_reply_shortcuts_on_server(vector shortcut_ids, Promise &&promise) { td_->create_handler(std::move(promise))->send(std::move(shortcut_ids)); } void QuickReplyManager::update_quick_reply_message(telegram_api::object_ptr &&message_ptr) { if (td_->auth_manager_->is_bot()) { return; } load_quick_reply_shortcuts(); auto message = create_message(std::move(message_ptr), "update_quick_reply_message"); if (message == nullptr) { return; } auto shortcut_id = message->shortcut_id; auto *s = get_shortcut(shortcut_id); if (s == nullptr) { return reload_quick_reply_messages(shortcut_id, Promise()); } on_get_quick_reply_message(s, std::move(message)); } void QuickReplyManager::on_get_quick_reply_message(Shortcut *s, unique_ptr message) { auto it = get_message_it(s, message->message_id); if (it == s->messages_.end()) { change_message_files({s->shortcut_id_, message->message_id}, message.get(), {}); s->messages_.push_back(std::move(message)); s->server_total_count_++; sort_quick_reply_messages(s->messages_); send_update_quick_reply_shortcut(s, "on_get_quick_reply_message 1"); } else { if (get_quick_reply_unique_id(it->get()) == get_quick_reply_unique_id(message.get())) { return; } update_quick_reply_message(s->shortcut_id_, *it, std::move(message)); if (it == s->messages_.begin()) { send_update_quick_reply_shortcut(s, "on_get_quick_reply_message 2"); } } send_update_quick_reply_shortcut_messages(s, "on_get_quick_reply_message 2"); save_quick_reply_shortcuts(); } void QuickReplyManager::update_quick_reply_message(QuickReplyShortcutId shortcut_id, unique_ptr &old_message, unique_ptr &&new_message) { CHECK(old_message != nullptr); CHECK(new_message != nullptr); CHECK(old_message->message_id == new_message->message_id); CHECK(old_message->message_id.is_server()); if (old_message->edit_date > new_message->edit_date) { LOG(INFO) << "Ignore update of " << old_message->message_id << " from " << shortcut_id << " to its old version"; return; } auto old_file_ids = get_message_file_ids(old_message.get()); new_message->edited_content = std::move(old_message->edited_content); new_message->edited_invert_media = old_message->edited_invert_media; new_message->edited_disable_web_page_preview = old_message->edited_disable_web_page_preview; new_message->edit_generation = old_message->edit_generation; old_message = std::move(new_message); change_message_files({shortcut_id, old_message->message_id}, old_message.get(), old_file_ids); } void QuickReplyManager::delete_quick_reply_messages_from_updates(QuickReplyShortcutId shortcut_id, const vector &message_ids) { if (td_->auth_manager_->is_bot()) { return; } load_quick_reply_shortcuts(); auto *s = get_shortcut(shortcut_id); if (s == nullptr) { return; } for (auto message_id : message_ids) { if (!message_id.is_server()) { LOG(ERROR) << "Receive delete of " << message_ids; return; } } delete_quick_reply_messages(s, message_ids, "delete_quick_reply_messages_from_updates"); } void QuickReplyManager::delete_quick_reply_messages(Shortcut *s, const vector &message_ids, const char *source) { LOG(INFO) << "Delete " << message_ids << " from " << s->shortcut_id_ << " from " << source; bool is_changed = false; for (auto &message_id : message_ids) { auto it = get_message_it(s, message_id); if (it != s->messages_.end()) { delete_message_files(s->shortcut_id_, it->get()); if (message_id.is_server()) { s->server_total_count_--; } else { s->local_total_count_--; } is_changed = true; s->messages_.erase(it); } if (message_id.is_server()) { deleted_message_full_ids_.insert({s->shortcut_id_, message_id}); } } if (s->messages_.empty()) { send_update_quick_reply_shortcut_deleted(s); shortcuts_.shortcuts_.erase(get_shortcut_it(s->shortcut_id_)); CHECK(is_changed); send_update_quick_reply_shortcuts(); save_quick_reply_shortcuts(); } else if (is_changed) { send_update_quick_reply_shortcut(s, source); send_update_quick_reply_shortcut_messages(s, source); save_quick_reply_shortcuts(); } } void QuickReplyManager::delete_quick_reply_shortcut_messages(QuickReplyShortcutId shortcut_id, const vector &message_ids, Promise &&promise) { load_quick_reply_shortcuts(); auto *s = get_shortcut(shortcut_id); if (s == nullptr) { return promise.set_error(Status::Error(400, "Shortcut not found")); } if (message_ids.empty()) { return promise.set_value(Unit()); } vector deleted_server_message_ids; for (auto &message_id : message_ids) { if (!message_id.is_valid()) { return promise.set_error(Status::Error(400, "Invalid message identifier")); } // message_id = get_persistent_message_id(s, message_id); if (message_id.is_server()) { deleted_server_message_ids.push_back(message_id); } } delete_quick_reply_messages_on_server(shortcut_id, std::move(deleted_server_message_ids), std::move(promise)); delete_quick_reply_messages(s, message_ids, "delete_quick_reply_shortcut_messages"); } void QuickReplyManager::delete_quick_reply_messages_on_server(QuickReplyShortcutId shortcut_id, const vector &message_ids, Promise &&promise) { if (message_ids.empty()) { return promise.set_value(Unit()); } td_->create_handler(std::move(promise))->send(shortcut_id, message_ids); } telegram_api::object_ptr QuickReplyManager::get_input_quick_reply_shortcut( QuickReplyShortcutId shortcut_id) const { if (shortcut_id.is_server()) { return telegram_api::make_object(shortcut_id.get()); } const auto *s = get_shortcut(shortcut_id); CHECK(s != nullptr); return telegram_api::make_object(s->name_); } Status QuickReplyManager::check_send_quick_reply_messages_response( QuickReplyShortcutId shortcut_id, const telegram_api::object_ptr &updates_ptr, const vector &random_ids) { if (updates_ptr->get_id() != telegram_api::updates::ID) { return Status::Error("Receive unexpected updates class"); } const auto &updates = static_cast(updates_ptr.get())->updates_; FlatHashSet sent_random_ids; for (auto &update : updates) { if (update->get_id() == telegram_api::updateMessageID::ID) { auto update_message_id = static_cast(update.get()); int64 random_id = update_message_id->random_id_; if (random_id == 0) { return Status::Error("Receive zero random_id"); } if (!sent_random_ids.insert(random_id).second) { return Status::Error("Receive duplicate random_id"); } } } if (sent_random_ids.size() != random_ids.size()) { return Status::Error("Receive duplicate random_id"); } for (auto random_id : random_ids) { if (sent_random_ids.count(random_id) != 1) { return Status::Error("Don't receive expected random_id"); } } int32 new_shortcut_count = 0; for (auto &update : updates) { if (update->get_id() == telegram_api::updateNewQuickReply::ID) { if (!QuickReplyShortcutId( static_cast(update.get())->quick_reply_->shortcut_id_) .is_server()) { return Status::Error("Receive unexpected new shortcut"); } new_shortcut_count++; } } if (new_shortcut_count >= 2 || (new_shortcut_count >= 1 && shortcut_id.is_server())) { return Status::Error("Receive unexpected number of new shortcuts"); } return Status::OK(); } void QuickReplyManager::process_send_quick_reply_updates(QuickReplyShortcutId shortcut_id, telegram_api::object_ptr updates_ptr, vector random_ids) { auto check_status = check_send_quick_reply_messages_response(shortcut_id, updates_ptr, random_ids); if (check_status.is_error()) { LOG(ERROR) << check_status << " for sending messages " << random_ids << ": " << to_string(updates_ptr); on_failed_send_quick_reply_messages(shortcut_id, std::move(random_ids), Status::Error(500, "Receive wrong response")); return; } auto updates = telegram_api::move_object_as(updates_ptr); td_->user_manager_->on_get_users(std::move(updates->users_), "process_send_quick_reply_updates"); td_->chat_manager_->on_get_chats(std::move(updates->chats_), "process_send_quick_reply_updates"); bool is_shortcut_new = false; if (!shortcut_id.is_server()) { QuickReplyShortcutId new_shortcut_id; for (auto &update : updates->updates_) { if (update->get_id() == telegram_api::updateNewQuickReply::ID) { new_shortcut_id = QuickReplyShortcutId( static_cast(update.get())->quick_reply_->shortcut_id_); update = nullptr; } } if (!new_shortcut_id.is_server()) { for (const auto &update : updates->updates_) { if (update != nullptr && update->get_id() == telegram_api::updateQuickReplyMessage::ID) { auto &message = static_cast(update.get())->message_; if (message->get_id() == telegram_api::message::ID) { new_shortcut_id = QuickReplyShortcutId( static_cast(message.get())->quick_reply_shortcut_id_); break; } } } } if (!new_shortcut_id.is_server()) { LOG(ERROR) << "Failed to find new shortcut identifier for " << shortcut_id; reload_quick_reply_shortcuts(); on_failed_send_quick_reply_messages(shortcut_id, std::move(random_ids), Status::Error(500, "Receive wrong response")); return; } auto it = get_shortcut_it(shortcut_id); if (it != shortcuts_.shortcuts_.end()) { send_update_quick_reply_shortcut_deleted(it->get()); for (auto &message : (*it)->messages_) { CHECK(message->shortcut_id == shortcut_id); message->shortcut_id = new_shortcut_id; } auto *s = get_shortcut(new_shortcut_id); if (s == nullptr) { (*it)->shortcut_id_ = new_shortcut_id; is_shortcut_new = true; } else { if ((*it)->last_assigned_message_id_ > s->last_assigned_message_id_) { s->last_assigned_message_id_ = (*it)->last_assigned_message_id_; } for (auto &message : (*it)->messages_) { CHECK(!message->message_id.is_server()); s->messages_.push_back(std::move(message)); s->local_total_count_++; } shortcuts_.shortcuts_.erase(it); } } else if (get_shortcut(new_shortcut_id) == nullptr) { return; } shortcut_id = new_shortcut_id; } auto *s = get_shortcut(shortcut_id); CHECK(s != nullptr); for (auto &random_id : random_ids) { for (auto it = s->messages_.begin(); it != s->messages_.end(); ++it) { if ((*it)->random_id == random_id && (*it)->message_id.is_yet_unsent()) { MessageId new_message_id; for (auto &update : updates->updates_) { if (update != nullptr && update->get_id() == telegram_api::updateMessageID::ID && static_cast(update.get())->random_id_ == random_id) { new_message_id = MessageId(ServerMessageId(static_cast(update.get())->id_)); update = nullptr; } } if (new_message_id.is_valid()) { for (auto &update : updates->updates_) { if (update != nullptr && update->get_id() == telegram_api::updateQuickReplyMessage::ID && MessageId::get_message_id( static_cast(update.get())->message_, false) == new_message_id) { auto message = create_message( std::move(static_cast(update.get())->message_), "process_send_quick_reply_updates"); if (message != nullptr && message->shortcut_id == shortcut_id) { update_message_content(it->get(), message.get(), false); auto old_message_it = get_message_it(s, message->message_id); if (old_message_it == s->messages_.end()) { change_message_files({shortcut_id, message->message_id}, message.get(), {}); *it = std::move(message); s->server_total_count_++; } else { // the message has already been added update_quick_reply_message(shortcut_id, *old_message_it, std::move(message)); s->messages_.erase(it); } s->local_total_count_--; } update = nullptr; break; } } } break; } } } sort_quick_reply_messages(s->messages_); send_update_quick_reply_shortcut(s, "process_send_quick_reply_updates"); send_update_quick_reply_shortcut_messages(s, "process_send_quick_reply_updates"); if (is_shortcut_new) { send_update_quick_reply_shortcuts(); } save_quick_reply_shortcuts(); } void QuickReplyManager::update_message_content(QuickReplyMessage *old_message, QuickReplyMessage *new_message, bool is_edit) { CHECK(is_edit ? old_message->message_id.is_server() : old_message->message_id.is_yet_unsent()); CHECK(new_message->edited_content == nullptr); unique_ptr &old_content = is_edit ? old_message->edited_content : old_message->content; unique_ptr &new_content = new_message->content; MessageContentType old_content_type = old_content->get_type(); MessageContentType new_content_type = new_content->get_type(); auto old_file_id = get_message_content_any_file_id(old_content.get()); auto need_merge_files = (is_edit || new_message->edit_date == 0) && old_file_id.is_valid(); if (old_content_type != new_content_type) { if (need_merge_files) { td_->file_manager_->try_merge_documents(old_file_id, get_message_content_any_file_id(new_content.get())); } } else { bool is_content_changed = false; bool need_update = false; merge_message_contents(td_, old_content.get(), new_content.get(), true, DialogId(), need_merge_files, is_content_changed, need_update); } if (old_file_id.is_valid()) { // the file is likely to be already merged with a server file, but if not we need to // cancel file upload of the main file to allow next upload with the same file to succeed send_closure_later(G()->file_manager(), &FileManager::cancel_upload, old_file_id); update_message_content_file_id_remote(new_content.get(), old_file_id); } } void QuickReplyManager::on_failed_send_quick_reply_messages(QuickReplyShortcutId shortcut_id, vector random_ids, Status error) { auto *s = get_shortcut(shortcut_id); if (s == nullptr) { // the shortcut was deleted return; } for (auto &random_id : random_ids) { for (auto it = s->messages_.begin(); it != s->messages_.end(); ++it) { if ((*it)->random_id == random_id && (*it)->message_id.is_yet_unsent()) { auto old_message_id = (*it)->message_id; auto new_message_id = old_message_id.get_next_message_id(MessageType::Local); if (get_message_it(s, new_message_id) != s->messages_.end() || deleted_message_full_ids_.count({shortcut_id, new_message_id})) { new_message_id = get_next_local_message_id(s); } else if (new_message_id > s->last_assigned_message_id_) { s->last_assigned_message_id_ = new_message_id; } CHECK(new_message_id.is_valid()); (*it)->message_id = new_message_id; (*it)->is_failed_to_send = true; (*it)->send_error_code = error.code(); (*it)->send_error_message = error.message().str(); (*it)->try_resend_at = 0.0; auto retry_after = Global::get_retry_after((*it)->send_error_code, (*it)->send_error_message); if (retry_after > 0) { (*it)->try_resend_at = Time::now() + retry_after; } CHECK((*it)->edited_content == nullptr); update_failed_to_send_message_content(td_, (*it)->content); break; } } } sort_quick_reply_messages(s->messages_); send_update_quick_reply_shortcut(s, "on_failed_send_quick_reply_messages"); send_update_quick_reply_shortcut_messages(s, "on_failed_send_quick_reply_messages"); save_quick_reply_shortcuts(); } Result> QuickReplyManager::send_message( const string &shortcut_name, MessageId reply_to_message_id, td_api::object_ptr &&input_message_content) { TRY_RESULT(message_content, process_input_message_content(std::move(input_message_content))); TRY_RESULT(s, create_new_local_shortcut(shortcut_name, 1)); bool is_new = s->messages_.empty(); reply_to_message_id = get_input_reply_to_message_id(s, reply_to_message_id); auto content = dup_message_content(td_, td_->dialog_manager_->get_my_dialog_id(), message_content.content.get(), MessageContentDupType::Send, MessageCopyOptions()); auto *m = add_local_message(s, reply_to_message_id, std::move(content), message_content.invert_media, message_content.via_bot_user_id, false, message_content.disable_web_page_preview, std::move(message_content.emoji)); send_update_quick_reply_shortcut(s, "send_message"); send_update_quick_reply_shortcut_messages(s, "send_message"); if (is_new) { send_update_quick_reply_shortcuts(); } save_quick_reply_shortcuts(); do_send_message(m); return get_quick_reply_message_object(m, "send_message"); } Result> QuickReplyManager::send_inline_query_result_message( const string &shortcut_name, MessageId reply_to_message_id, int64 query_id, const string &result_id, bool hide_via_bot) { const InlineMessageContent *message_content = td_->inline_queries_manager_->get_inline_message_content(query_id, result_id); if (message_content == nullptr || query_id == 0) { return Status::Error(400, "Inline query result not found"); } TRY_RESULT(s, create_new_local_shortcut(shortcut_name, 1)); bool is_new = s->messages_.empty(); reply_to_message_id = get_input_reply_to_message_id(s, reply_to_message_id); UserId via_bot_user_id; if (!hide_via_bot) { via_bot_user_id = td_->inline_queries_manager_->get_inline_bot_user_id(query_id); } auto content = dup_message_content(td_, td_->dialog_manager_->get_my_dialog_id(), message_content->message_content.get(), MessageContentDupType::SendViaBot, MessageCopyOptions()); auto *m = add_local_message(s, reply_to_message_id, std::move(content), message_content->invert_media, via_bot_user_id, hide_via_bot, message_content->disable_web_page_preview, string()); m->reply_markup = dup_reply_markup(message_content->message_reply_markup); m->inline_query_id = query_id; m->inline_result_id = result_id; send_update_quick_reply_shortcut(s, "send_inline_query_result_message"); send_update_quick_reply_shortcut_messages(s, "send_inline_query_result_message"); if (is_new) { send_update_quick_reply_shortcuts(); } save_quick_reply_shortcuts(); do_send_message(m); return get_quick_reply_message_object(m, "send_inline_query_result_message"); } void QuickReplyManager::do_send_message(const QuickReplyMessage *m, vector bad_parts) { bool is_edit = m->message_id.is_server(); auto message_full_id = QuickReplyMessageFullId(m->shortcut_id, m->message_id); LOG(INFO) << "Do " << (is_edit ? "edit" : "send") << ' ' << message_full_id; auto content = is_edit ? m->edited_content.get() : m->content.get(); CHECK(content != nullptr); auto content_type = content->get_type(); if (content_type == MessageContentType::Unsupported) { if (is_edit) { return fail_edit_quick_reply_message(m->shortcut_id, m->message_id, m->edit_generation, FileId(), FileId(), string(), false, false, Status::Error(400, "Failed to upload file")); } return on_failed_send_quick_reply_messages(m->shortcut_id, {m->random_id}, Status::Error(400, "Failed to upload file")); } if (!is_edit && m->inline_query_id != 0) { td_->create_handler()->send(m); return; } if (content_type == MessageContentType::Text) { if (is_edit) { td_->create_handler()->send(FileId(), FileId(), m, nullptr); return; } auto input_media = get_message_content_input_media_web_page(td_, content); if (input_media == nullptr) { td_->create_handler()->send(m); } else { td_->create_handler()->send(FileId(), FileId(), m, std::move(input_media)); } return; } FileId file_id = get_message_content_any_file_id(content); // any_file_id, because it could be a photo sent by ID FileId thumbnail_file_id = get_message_content_thumbnail_file_id(content, td_); LOG(DEBUG) << "Need to send file " << file_id << " with thumbnail " << thumbnail_file_id; auto input_media = get_input_media(content, td_, {}, m->send_emoji, false); if (input_media == nullptr) { if (content_type == MessageContentType::Game || content_type == MessageContentType::Story) { return; } CHECK(file_id.is_valid()); FileView file_view = td_->file_manager_->get_file_view(file_id); if (get_main_file_type(file_view.get_type()) == FileType::Photo) { thumbnail_file_id = FileId(); } LOG(INFO) << "Ask to upload file " << file_id << " with bad parts " << bad_parts; CHECK(file_id.is_valid()); bool is_inserted = being_uploaded_files_.emplace(file_id, std::make_tuple(message_full_id, thumbnail_file_id, m->edit_generation)) .second; CHECK(is_inserted); td_->file_manager_->resume_upload(file_id, std::move(bad_parts), upload_media_callback_, 1, m->message_id.get()); } else { on_message_media_uploaded(m, std::move(input_media), file_id, thumbnail_file_id); } } void QuickReplyManager::on_send_message_file_parts_missing(QuickReplyShortcutId shortcut_id, int64 random_id, vector &&bad_parts) { auto *s = get_shortcut(shortcut_id); if (s != nullptr) { for (auto &message : s->messages_) { if (message->random_id == random_id && message->message_id.is_yet_unsent()) { do_send_message(message.get(), std::move(bad_parts)); } } } } void QuickReplyManager::on_send_message_file_reference_error(QuickReplyShortcutId shortcut_id, int64 random_id) { auto *s = get_shortcut(shortcut_id); if (s != nullptr) { for (auto &message : s->messages_) { if (message->random_id == random_id && message->message_id.is_yet_unsent()) { do_send_message(message.get(), {-1}); } } } } void QuickReplyManager::on_upload_media(FileId file_id, telegram_api::object_ptr input_file) { LOG(INFO) << "File " << file_id << " has been uploaded"; auto it = being_uploaded_files_.find(file_id); CHECK(it != being_uploaded_files_.end()); auto message_full_id = std::get<0>(it->second); auto thumbnail_file_id = std::get<1>(it->second); auto edit_generation = std::get<2>(it->second); being_uploaded_files_.erase(it); auto *m = get_message(message_full_id); if (m == nullptr || (m->message_id.is_server() && m->edit_generation != edit_generation)) { send_closure_later(G()->file_manager(), &FileManager::cancel_upload, file_id); return; } if (input_file && thumbnail_file_id.is_valid()) { // TODO: download thumbnail if needed (like in secret chats) LOG(INFO) << "Ask to upload thumbnail " << thumbnail_file_id; bool is_inserted = being_uploaded_thumbnails_ .emplace(thumbnail_file_id, UploadedThumbnailInfo{message_full_id, file_id, std::move(input_file), edit_generation}) .second; CHECK(is_inserted); td_->file_manager_->upload(thumbnail_file_id, upload_thumbnail_callback_, 32, m->message_id.get()); } else { do_send_media(m, file_id, thumbnail_file_id, std::move(input_file), nullptr); } } void QuickReplyManager::do_send_media(QuickReplyMessage *m, FileId file_id, FileId thumbnail_file_id, telegram_api::object_ptr input_file, telegram_api::object_ptr input_thumbnail) { CHECK(m != nullptr); bool have_input_file = input_file != nullptr; bool have_input_thumbnail = input_thumbnail != nullptr; LOG(INFO) << "Do send media file " << file_id << " with thumbnail " << thumbnail_file_id << ", have_input_file = " << have_input_file << ", have_input_thumbnail = " << have_input_thumbnail; auto content = m->message_id.is_server() ? m->edited_content.get() : m->content.get(); CHECK(content != nullptr); auto input_media = get_input_media(content, td_, std::move(input_file), std::move(input_thumbnail), file_id, thumbnail_file_id, {}, m->send_emoji, true); CHECK(input_media != nullptr); on_message_media_uploaded(m, std::move(input_media), file_id, thumbnail_file_id); } void QuickReplyManager::on_upload_media_error(FileId file_id, Status status) { if (G()->close_flag()) { // do not fail upload if closing return; } LOG(WARNING) << "File " << file_id << " has upload error " << status; CHECK(status.is_error()); auto it = being_uploaded_files_.find(file_id); CHECK(it != being_uploaded_files_.end()); auto message_full_id = std::get<0>(it->second); being_uploaded_files_.erase(it); auto *m = get_message(message_full_id); if (m == nullptr) { return; } on_failed_send_quick_reply_messages(message_full_id.get_quick_reply_shortcut_id(), {m->random_id}, std::move(status)); } void QuickReplyManager::on_upload_thumbnail(FileId thumbnail_file_id, telegram_api::object_ptr thumbnail_input_file) { if (G()->close_flag()) { // do not fail upload if closing return; } LOG(INFO) << "Thumbnail " << thumbnail_file_id << " has been uploaded as " << to_string(thumbnail_input_file); auto it = being_uploaded_thumbnails_.find(thumbnail_file_id); CHECK(it != being_uploaded_thumbnails_.end()); auto message_full_id = it->second.quick_reply_message_full_id; auto file_id = it->second.file_id; auto input_file = std::move(it->second.input_file); auto edit_generation = it->second.edit_generation; being_uploaded_thumbnails_.erase(it); auto *m = get_message(message_full_id); if (m == nullptr || (m->message_id.is_server() && m->edit_generation != edit_generation)) { send_closure_later(G()->file_manager(), &FileManager::cancel_upload, file_id); send_closure_later(G()->file_manager(), &FileManager::cancel_upload, thumbnail_file_id); return; } if (thumbnail_input_file == nullptr) { auto content = m->message_id.is_server() ? m->edited_content.get() : m->content.get(); delete_message_content_thumbnail(content, td_); } do_send_media(m, file_id, thumbnail_file_id, std::move(input_file), std::move(thumbnail_input_file)); } void QuickReplyManager::on_message_media_uploaded(const QuickReplyMessage *m, telegram_api::object_ptr &&input_media, FileId file_id, FileId thumbnail_file_id) { if (G()->close_flag()) { return; } CHECK(m != nullptr); CHECK(input_media != nullptr); auto message_id = m->message_id; if (message_id.is_any_server()) { CHECK(m->edited_content != nullptr); CHECK(m->edited_content->get_type() != MessageContentType::Text); td_->create_handler()->send(file_id, thumbnail_file_id, m, std::move(input_media)); return; } CHECK(m->media_album_id == 0); td_->create_handler()->send(file_id, thumbnail_file_id, m, std::move(input_media)); } int64 QuickReplyManager::generate_new_media_album_id() { int64 media_album_id = 0; do { media_album_id = Random::secure_int64(); } while (media_album_id >= 0); return media_album_id; } Result> QuickReplyManager::resend_messages( const string &shortcut_name, vector message_ids) { if (message_ids.empty()) { return Status::Error(400, "There are no messages to resend"); } load_quick_reply_shortcuts(); auto *s = get_shortcut(shortcut_name); if (s == nullptr) { return Status::Error(400, "Quick reply shortcut not found"); } MessageId last_message_id; for (auto &message_id : message_ids) { const auto *m = get_message(s, message_id); if (m == nullptr) { return Status::Error(400, "Message not found"); } if (!m->is_failed_to_send) { return Status::Error(400, "Message is not failed to send"); } if (!can_resend_quick_reply_message(m)) { return Status::Error(400, "Message can't be re-sent"); } if (m->try_resend_at > Time::now()) { return Status::Error(400, "Message can't be re-sent yet"); } if (last_message_id != MessageId() && m->message_id <= last_message_id) { return Status::Error(400, "Message identifiers must be in a strictly increasing order"); } last_message_id = m->message_id; } vector> new_contents(message_ids.size()); std::unordered_map, Hash> new_media_album_ids; for (size_t i = 0; i < message_ids.size(); i++) { MessageId message_id = message_ids[i]; const auto *m = get_message(s, message_id); CHECK(m != nullptr); CHECK(m->edited_content == nullptr); unique_ptr content = dup_message_content(td_, td_->dialog_manager_->get_my_dialog_id(), m->content.get(), m->inline_query_id != 0 ? MessageContentDupType::SendViaBot : MessageContentDupType::Send, MessageCopyOptions()); if (content == nullptr) { LOG(INFO) << "Can't resend " << m->message_id; continue; } new_contents[i] = std::move(content); if (m->media_album_id != 0) { auto &new_media_album_id = new_media_album_ids[m->media_album_id]; new_media_album_id.second++; if (new_media_album_id.second == 2) { // have at least 2 messages in the new album CHECK(new_media_album_id.first == 0); new_media_album_id.first = generate_new_media_album_id(); } if (new_media_album_id.second == MAX_GROUPED_MESSAGES + 1) { CHECK(new_media_album_id.first != 0); new_media_album_id.first = 0; // just in case } } } bool is_changed = false; auto result = td_api::make_object(); for (size_t i = 0; i < message_ids.size(); i++) { if (new_contents[i] == nullptr) { result->messages_.push_back(nullptr); continue; } auto *m = get_message(s, message_ids[i]); CHECK(m != nullptr); m->message_id = get_next_yet_unsent_message_id(s); m->media_album_id = new_media_album_ids[m->media_album_id].first; m->is_failed_to_send = false; m->send_error_code = 0; m->send_error_message = string(); m->try_resend_at = 0.0; do_send_message(m); result->messages_.push_back(get_quick_reply_message_object(m, "resend_message")); is_changed = true; } if (is_changed) { sort_quick_reply_messages(s->messages_); send_update_quick_reply_shortcut(s, "resend_message"); send_update_quick_reply_shortcut_messages(s, "resend_message"); save_quick_reply_shortcuts(); } return result; } void QuickReplyManager::edit_quick_reply_message( QuickReplyShortcutId shortcut_id, MessageId message_id, td_api::object_ptr &&input_message_content, Promise &&promise) { load_quick_reply_shortcuts(); auto *s = get_shortcut(shortcut_id); if (s == nullptr) { return promise.set_error(Status::Error(400, "Shortcut not found")); } auto *m = get_message(s, message_id); if (m == nullptr) { return promise.set_error(Status::Error(400, "Message not found")); } if (!can_edit_quick_reply_message(m)) { return promise.set_error(Status::Error(400, "Message can't be edited")); } TRY_RESULT_PROMISE(promise, message_content, process_input_message_content(std::move(input_message_content))); auto new_message_content_type = message_content.content->get_type(); auto old_message_content_type = m->content->get_type(); switch (old_message_content_type) { case MessageContentType::Text: if (new_message_content_type != MessageContentType::Text) { return promise.set_error(Status::Error(400, "Text messages can be edited only to text messages")); } break; case MessageContentType::Animation: case MessageContentType::Audio: case MessageContentType::Document: case MessageContentType::Photo: case MessageContentType::Video: if (new_message_content_type != MessageContentType::Animation && new_message_content_type != MessageContentType::Audio && new_message_content_type != MessageContentType::Document && new_message_content_type != MessageContentType::Photo && new_message_content_type != MessageContentType::Video) { return promise.set_error(Status::Error(400, "Media messages can be edited only to media messages")); } if (m->media_album_id != 0) { if (old_message_content_type != new_message_content_type) { if (!is_allowed_media_group_content(new_message_content_type)) { return promise.set_error(Status::Error(400, "Message content type can't be used in an album")); } if (is_homogenous_media_group_content(old_message_content_type) || is_homogenous_media_group_content(new_message_content_type)) { return promise.set_error(Status::Error(400, "Can't change media type in the album")); } } } break; case MessageContentType::VoiceNote: if (new_message_content_type != MessageContentType::VoiceNote || get_message_content_any_file_id(m->content.get()) != get_message_content_any_file_id(message_content.content.get())) { return promise.set_error(Status::Error(400, "Only caption can be edited in voice note messages")); } break; default: UNREACHABLE(); } auto old_file_ids = get_message_file_ids(m); m->edited_content = dup_message_content(td_, td_->dialog_manager_->get_my_dialog_id(), message_content.content.get(), MessageContentDupType::Send, MessageCopyOptions()); CHECK(m->edited_content != nullptr); m->edited_invert_media = message_content.invert_media; m->edited_disable_web_page_preview = message_content.disable_web_page_preview; m->edit_generation = ++current_message_edit_generation_; change_message_files({shortcut_id, message_id}, m, old_file_ids); if (s->messages_[0]->message_id == message_id) { send_update_quick_reply_shortcut(s, "edit_quick_reply_message 1"); } send_update_quick_reply_shortcut_messages(s, "edit_quick_reply_message 2"); save_quick_reply_shortcuts(); do_send_message(m); promise.set_value(Unit()); } void QuickReplyManager::on_edit_quick_reply_message(QuickReplyShortcutId shortcut_id, MessageId message_id, int64 edit_generation, FileId file_id, bool was_uploaded, telegram_api::object_ptr updates_ptr) { auto *s = get_shortcut(shortcut_id); auto *m = get_message(s, message_id); if (m == nullptr) { if (was_uploaded) { send_closure_later(G()->file_manager(), &FileManager::cancel_upload, file_id); } return; } if (m->edit_generation != edit_generation) { LOG(INFO) << "Ignore successful edit of " << QuickReplyMessageFullId(m->shortcut_id, m->message_id) << " with generation " << edit_generation << " instead of " << m->edit_generation; if (was_uploaded) { send_closure_later(G()->file_manager(), &FileManager::cancel_upload, file_id); } return; } LOG(INFO) << "Receive result for editing of " << QuickReplyMessageFullId(m->shortcut_id, m->message_id) << ": " << to_string(updates_ptr); bool was_updated = updates_ptr == nullptr; if (updates_ptr == nullptr || updates_ptr->get_id() != telegram_api::updates::ID) { if (was_uploaded) { send_closure_later(G()->file_manager(), &FileManager::cancel_upload, file_id); } reload_quick_reply_message(shortcut_id, message_id, Promise()); } else { auto updates = telegram_api::move_object_as(updates_ptr); td_->user_manager_->on_get_users(std::move(updates->users_), "on_edit_quick_reply_message"); td_->chat_manager_->on_get_chats(std::move(updates->chats_), "on_edit_quick_reply_message"); if (updates->updates_.size() != 1 || updates->updates_[0]->get_id() != telegram_api::updateQuickReplyMessage::ID) { LOG(ERROR) << "Receive " << to_string(updates); if (was_uploaded) { send_closure_later(G()->file_manager(), &FileManager::cancel_upload, file_id); } } else { auto update_message = telegram_api::move_object_as(updates->updates_[0]); auto message = create_message(std::move(update_message->message_), "on_edit_quick_reply_message"); if (message == nullptr || message->shortcut_id != shortcut_id || message->message_id != message_id) { LOG(ERROR) << "Receive unexpected message"; if (was_uploaded) { send_closure_later(G()->file_manager(), &FileManager::cancel_upload, file_id); } } else { update_message_content(m, message.get(), true); auto old_message_it = get_message_it(s, message_id); update_quick_reply_message(shortcut_id, *old_message_it, std::move(message)); m = old_message_it->get(); was_updated = true; } } } auto old_file_ids = get_message_file_ids(m); CHECK(m->edited_content != nullptr); if (!was_updated) { m->content = std::move(m->edited_content); m->invert_media = m->edited_invert_media; m->disable_web_page_preview = m->edited_disable_web_page_preview; } m->edit_generation = 0; m->edited_content = nullptr; m->edited_invert_media = false; m->edited_disable_web_page_preview = false; change_message_files({shortcut_id, message_id}, m, old_file_ids); if (s->messages_[0]->message_id == m->message_id) { send_update_quick_reply_shortcut(s, "on_edit_quick_reply_message 1"); } send_update_quick_reply_shortcut_messages(s, "on_edit_quick_reply_message 2"); save_quick_reply_shortcuts(); } void QuickReplyManager::fail_edit_quick_reply_message(QuickReplyShortcutId shortcut_id, MessageId message_id, int64 edit_generation, FileId file_id, FileId thumbnail_file_id, string file_reference, bool was_uploaded, bool was_thumbnail_uploaded, Status status) { auto *m = get_message({shortcut_id, message_id}); if (m == nullptr) { if (was_uploaded) { send_closure_later(G()->file_manager(), &FileManager::cancel_upload, file_id); } return; } if (m->edit_generation != edit_generation) { LOG(INFO) << "Ignore failed edit of " << QuickReplyMessageFullId(m->shortcut_id, m->message_id) << " with generation " << edit_generation << " instead of " << m->edit_generation; if (was_uploaded) { send_closure_later(G()->file_manager(), &FileManager::cancel_upload, file_id); } return; } if (was_uploaded) { if (was_thumbnail_uploaded) { CHECK(thumbnail_file_id.is_valid()); // always delete partial remote location for the thumbnail, because it can't be reused anyway td_->file_manager_->delete_partial_remote_location(thumbnail_file_id); } CHECK(file_id.is_valid()); auto bad_parts = FileManager::get_missing_file_parts(status); if (!bad_parts.empty()) { do_send_message(m, std::move(bad_parts)); return; } else { td_->file_manager_->delete_partial_remote_location_if_needed(file_id, status); } } else if (FileReferenceManager::is_file_reference_error(status)) { if (file_id.is_valid() && !was_uploaded) { VLOG(file_references) << "Receive " << status << " for " << file_id; td_->file_manager_->delete_file_reference(file_id, file_reference); do_send_message(m, {-1}); return; } else { LOG(ERROR) << "Receive file reference error, but file_id = " << file_id << ", was_uploaded = " << was_uploaded; } } auto old_file_ids = get_message_file_ids(m); m->edit_generation = 0; m->edited_content = nullptr; m->edited_invert_media = false; m->edited_disable_web_page_preview = false; change_message_files({m->shortcut_id, m->message_id}, m, old_file_ids); auto *s = get_shortcut(m->shortcut_id); CHECK(s != nullptr); if (s->messages_[0]->message_id == m->message_id) { send_update_quick_reply_shortcut(s, "fail_edit_quick_reply_message 1"); } send_update_quick_reply_shortcut_messages(s, "fail_edit_quick_reply_message 2"); save_quick_reply_shortcuts(); reload_quick_reply_message(shortcut_id, message_id, Promise()); } void QuickReplyManager::get_quick_reply_shortcut_messages(QuickReplyShortcutId shortcut_id, Promise &&promise) { load_quick_reply_shortcuts(); auto *s = get_shortcut(shortcut_id); if (s == nullptr) { return promise.set_error(Status::Error(400, "Shortcut not found")); } if (have_all_shortcut_messages(s)) { return promise.set_value(Unit()); } CHECK(shortcut_id.is_server()); reload_quick_reply_messages(shortcut_id, std::move(promise)); } void QuickReplyManager::reload_quick_reply_messages(QuickReplyShortcutId shortcut_id, Promise &&promise) { if (td_->auth_manager_->is_bot()) { return promise.set_error(Status::Error(400, "Not supported by bots")); } load_quick_reply_shortcuts(); CHECK(shortcut_id.is_valid()); if (!shortcut_id.is_server()) { return promise.set_value(Unit()); } auto &queries = get_shortcut_messages_queries_[shortcut_id]; queries.push_back(std::move(promise)); if (queries.size() != 1) { return; } auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), shortcut_id]( Result> r_messages) { send_closure(actor_id, &QuickReplyManager::on_reload_quick_reply_messages, shortcut_id, std::move(r_messages)); }); td_->create_handler(std::move(query_promise)) ->send(shortcut_id, vector(), get_quick_reply_messages_hash(get_shortcut(shortcut_id))); } void QuickReplyManager::on_reload_quick_reply_messages( QuickReplyShortcutId shortcut_id, Result> r_messages) { G()->ignore_result_if_closing(r_messages); auto queries_it = get_shortcut_messages_queries_.find(shortcut_id); CHECK(queries_it != get_shortcut_messages_queries_.end()); CHECK(!queries_it->second.empty()); auto promises = std::move(queries_it->second); get_shortcut_messages_queries_.erase(queries_it); if (r_messages.is_error()) { return fail_promises(promises, r_messages.move_as_error()); } auto messages_ptr = r_messages.move_as_ok(); switch (messages_ptr->get_id()) { case telegram_api::messages_messagesSlice::ID: case telegram_api::messages_channelMessages::ID: LOG(ERROR) << "Receive " << to_string(messages_ptr); break; case telegram_api::messages_messagesNotModified::ID: break; case telegram_api::messages_messages::ID: { auto messages = telegram_api::move_object_as(messages_ptr); td_->user_manager_->on_get_users(std::move(messages->users_), "on_reload_quick_reply_messages"); td_->chat_manager_->on_get_chats(std::move(messages->chats_), "on_reload_quick_reply_messages"); vector> quick_reply_messages; for (auto &server_message : messages->messages_) { auto message = create_message(std::move(server_message), "on_reload_quick_reply_messages"); if (message == nullptr) { continue; } if (message->shortcut_id != shortcut_id) { LOG(ERROR) << "Receive message from " << message->shortcut_id << " instead of " << shortcut_id; continue; } quick_reply_messages.push_back(std::move(message)); } auto it = get_shortcut_it(shortcut_id); if (quick_reply_messages.empty()) { if (it != shortcuts_.shortcuts_.end()) { send_update_quick_reply_shortcut_deleted(it->get()); shortcuts_.shortcuts_.erase(it); save_quick_reply_shortcuts(); send_update_quick_reply_shortcuts(); } break; } auto *old_shortcut = it != shortcuts_.shortcuts_.end() ? it->get() : nullptr; auto shortcut = td::make_unique(); shortcut->name_ = old_shortcut->name_; shortcut->shortcut_id_ = shortcut_id; shortcut->server_total_count_ = static_cast(quick_reply_messages.size()); shortcut->messages_ = std::move(quick_reply_messages); if (old_shortcut == nullptr) { CHECK(have_all_shortcut_messages(shortcut.get())); send_update_quick_reply_shortcut(shortcut.get(), "on_reload_quick_reply_messages 1"); send_update_quick_reply_shortcut_messages(shortcut.get(), "on_reload_quick_reply_messages 2"); for (auto &message : shortcut->messages_) { change_message_files({shortcut_id, message->message_id}, message.get(), {}); } shortcuts_.shortcuts_.push_back(std::move(shortcut)); } else { bool is_shortcut_changed = false; bool are_messages_changed = false; update_shortcut_from(shortcut.get(), old_shortcut, false, &is_shortcut_changed, &are_messages_changed); CHECK(have_all_shortcut_messages(shortcut.get())); if (is_shortcut_changed) { send_update_quick_reply_shortcut(shortcut.get(), "on_reload_quick_reply_messages 3"); } if (are_messages_changed) { send_update_quick_reply_shortcut_messages(shortcut.get(), "on_reload_quick_reply_messages 4"); } *it = std::move(shortcut); } save_quick_reply_shortcuts(); break; } default: UNREACHABLE(); } auto *s = get_shortcut(shortcut_id); if (s == nullptr) { return fail_promises(promises, Status::Error(400, "Shortcut not found")); } set_promises(promises); } int64 QuickReplyManager::get_quick_reply_messages_hash(const Shortcut *s) { if (s == nullptr) { return 0; } vector numbers; for (const auto &message : s->messages_) { if (message->message_id.is_server()) { numbers.push_back(message->message_id.get_server_message_id().get()); numbers.push_back(message->edit_date); } } return get_vector_hash(numbers); } void QuickReplyManager::reload_quick_reply_message(QuickReplyShortcutId shortcut_id, MessageId message_id, Promise &&promise) { if (td_->auth_manager_->is_bot()) { return promise.set_error(Status::Error(400, "Not supported by bots")); } load_quick_reply_shortcuts(); auto *s = get_shortcut(shortcut_id); if (s == nullptr) { return promise.set_error(Status::Error(400, "Shortcut not found")); } if (!message_id.is_server()) { return promise.set_error(Status::Error(400, "Message can't be reloaded")); } auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), shortcut_id, message_id, promise = std::move(promise)]( Result> r_messages) mutable { send_closure(actor_id, &QuickReplyManager::on_reload_quick_reply_message, shortcut_id, message_id, std::move(r_messages), std::move(promise)); }); td_->create_handler(std::move(query_promise)) ->send(shortcut_id, vector{message_id}, 0); } void QuickReplyManager::on_reload_quick_reply_message( QuickReplyShortcutId shortcut_id, MessageId message_id, Result> r_messages, Promise &&promise) { G()->ignore_result_if_closing(r_messages); if (r_messages.is_error()) { return promise.set_error(r_messages.move_as_error()); } auto *s = get_shortcut(shortcut_id); if (s == nullptr) { return promise.set_error(Status::Error(400, "Shortcut not found")); } auto messages_ptr = r_messages.move_as_ok(); switch (messages_ptr->get_id()) { case telegram_api::messages_messagesSlice::ID: case telegram_api::messages_channelMessages::ID: case telegram_api::messages_messagesNotModified::ID: LOG(ERROR) << "Receive " << to_string(messages_ptr); return promise.set_error(Status::Error(400, "Receive wrong response")); case telegram_api::messages_messages::ID: { auto messages = telegram_api::move_object_as(messages_ptr); td_->user_manager_->on_get_users(std::move(messages->users_), "on_reload_quick_reply_message"); td_->chat_manager_->on_get_chats(std::move(messages->chats_), "on_reload_quick_reply_message"); if (messages->messages_.size() > 1u) { LOG(ERROR) << "Receive " << to_string(messages_ptr); break; } unique_ptr message; if (messages->messages_.size() == 1u) { message = create_message(std::move(messages->messages_[0]), "on_reload_quick_reply_message"); } if (message == nullptr) { delete_quick_reply_messages(s, {message_id}, "on_reload_quick_reply_message"); return promise.set_error(Status::Error(400, "Message not found")); } if (message->shortcut_id != shortcut_id) { LOG(ERROR) << "Receive message from " << message->shortcut_id << " instead of " << shortcut_id; return promise.set_error(Status::Error(400, "Message not found")); } on_get_quick_reply_message(s, std::move(message)); break; } default: UNREACHABLE(); } promise.set_value(Unit()); } Result> QuickReplyManager::get_quick_reply_message_contents( DialogId dialog_id, QuickReplyShortcutId shortcut_id) const { const auto *shortcut = get_shortcut(shortcut_id); if (shortcut == nullptr) { return Status::Error(400, "Shortcut not found"); } if (!shortcut_id.is_server()) { return Status::Error(400, "Shortcut isn't created yet"); } if (!have_all_shortcut_messages(shortcut)) { return Status::Error(400, "Shortcut messages aren't loaded yet"); } TRY_STATUS(td_->dialog_manager_->check_dialog_access(dialog_id, false, AccessRights::Write, "get_quick_reply_message_contents")); if (dialog_id.get_type() != DialogType::User || td_->user_manager_->is_user_bot(dialog_id.get_user_id())) { return Status::Error(400, "Can't use quick replies in the chat"); } vector result; for (auto &message : shortcut->messages_) { if (!message->message_id.is_server()) { continue; } auto content = dup_message_content(td_, dialog_id, message->content.get(), MessageContentDupType::ServerCopy, MessageCopyOptions(true, false)); auto can_send_status = can_send_message_content(dialog_id, content.get(), false, true, td_); if (can_send_status.is_error()) { LOG(INFO) << "Can't send " << message->message_id << ": " << can_send_status.message(); // if we skip the message, the sending will fail anyway with MESSAGE_IDS_MISMATCH // continue; } auto disable_web_page_preview = message->disable_web_page_preview && content->get_type() == MessageContentType::Text && !has_message_content_web_page(content.get()); result.push_back({std::move(content), message->message_id, message->reply_to_message_id, dup_reply_markup(message->reply_markup), message->hide_via_bot ? UserId() : message->via_bot_user_id, message->media_album_id, message->invert_media, disable_web_page_preview}); } return std::move(result); } QuickReplyManager::Shortcut *QuickReplyManager::get_shortcut(QuickReplyShortcutId shortcut_id) { if (!shortcuts_.are_inited_) { return nullptr; } for (auto &shortcut : shortcuts_.shortcuts_) { if (shortcut->shortcut_id_ == shortcut_id) { return shortcut.get(); } } return nullptr; } const QuickReplyManager::Shortcut *QuickReplyManager::get_shortcut(QuickReplyShortcutId shortcut_id) const { if (!shortcuts_.are_inited_) { return nullptr; } for (auto &shortcut : shortcuts_.shortcuts_) { if (shortcut->shortcut_id_ == shortcut_id) { return shortcut.get(); } } return nullptr; } QuickReplyManager::Shortcut *QuickReplyManager::get_shortcut(const string &name) { if (!shortcuts_.are_inited_) { return nullptr; } for (auto &shortcut : shortcuts_.shortcuts_) { if (shortcut->name_ == name) { return shortcut.get(); } } return nullptr; } vector>::iterator QuickReplyManager::get_shortcut_it( QuickReplyShortcutId shortcut_id) { for (auto it = shortcuts_.shortcuts_.begin(); it != shortcuts_.shortcuts_.end(); ++it) { if (*it != nullptr && (*it)->shortcut_id_ == shortcut_id) { return it; } } return shortcuts_.shortcuts_.end(); } vector>::iterator QuickReplyManager::get_message_it( Shortcut *s, MessageId message_id) { CHECK(s != nullptr); for (auto it = s->messages_.begin(); it != s->messages_.end(); ++it) { if ((*it)->message_id == message_id) { return it; } } return s->messages_.end(); } QuickReplyManager::QuickReplyMessage *QuickReplyManager::get_message(QuickReplyMessageFullId message_full_id) { auto *s = get_shortcut(message_full_id.get_quick_reply_shortcut_id()); return get_message(s, message_full_id.get_message_id()); } QuickReplyManager::QuickReplyMessage *QuickReplyManager::get_message(Shortcut *s, MessageId message_id) { if (s != nullptr) { for (auto it = s->messages_.begin(); it != s->messages_.end(); ++it) { if ((*it)->message_id == message_id) { return it->get(); } } } return nullptr; } Result QuickReplyManager::create_new_local_shortcut(const string &name, int32 new_message_count) { TRY_STATUS(check_shortcut_name(name)); load_quick_reply_shortcuts(); if (!shortcuts_.are_inited_) { return Status::Error(400, "Quick reply shortcuts must be loaded first"); } auto *s = get_shortcut(name); auto max_message_count = td_->option_manager_->get_option_integer("quick_reply_shortcut_message_count_max"); if (s != nullptr) { if (!have_all_shortcut_messages(s)) { return Status::Error(400, "The quick reply shortcut must be loaded first"); } max_message_count -= s->server_total_count_ + s->local_total_count_; } else { auto max_shortcut_count = td_->option_manager_->get_option_integer("quick_reply_shortcut_count_max"); if (static_cast(shortcuts_.shortcuts_.size()) >= max_shortcut_count) { return Status::Error(400, "Quick reply shortcut count exceeded"); } } if (new_message_count > max_message_count) { return Status::Error(400, "Quick reply message count exceeded"); } if (s != nullptr) { return s; } if (next_local_shortcut_id_ >= std::numeric_limits::max() - 10) { return Status::Error(400, "Too many local shortcuts created"); } auto shortcut = td::make_unique(); shortcut->name_ = name; shortcut->shortcut_id_ = QuickReplyShortcutId(next_local_shortcut_id_++); s = shortcut.get(); shortcuts_.shortcuts_.push_back(std::move(shortcut)); return s; } MessageId QuickReplyManager::get_input_reply_to_message_id(const Shortcut *s, MessageId reply_to_message_id) { if (s == nullptr || !reply_to_message_id.is_valid() || !reply_to_message_id.is_server()) { return MessageId(); } for (auto &message : s->messages_) { CHECK(message != nullptr); if (message->message_id == reply_to_message_id) { return reply_to_message_id; } } return MessageId(); } Result QuickReplyManager::process_input_message_content( td_api::object_ptr &&input_message_content) { if (input_message_content == nullptr) { return Status::Error(400, "Can't add quick reply without content"); } if (input_message_content->get_id() == td_api::inputMessageForwarded::ID) { return Status::Error(400, "Can't forward messages to quick replies"); } if (input_message_content->get_id() == td_api::inputMessagePoll::ID) { return Status::Error(400, "Can't add poll as a quick reply"); } if (input_message_content->get_id() == td_api::inputMessageLocation::ID && static_cast(input_message_content.get())->live_period_ != 0) { return Status::Error(400, "Can't add live location as a quick reply"); } return get_input_message_content(DialogId(), std::move(input_message_content), td_, true); } MessageId QuickReplyManager::get_next_message_id(Shortcut *s, MessageType type) const { CHECK(s != nullptr); MessageId last_message_id = s->last_assigned_message_id_; if (!s->messages_.empty() && s->messages_.back() != nullptr && s->messages_.back()->message_id > last_message_id) { last_message_id = s->messages_.back()->message_id; } s->last_assigned_message_id_ = last_message_id.get_next_message_id(type); CHECK(s->last_assigned_message_id_.is_valid()); return s->last_assigned_message_id_; } MessageId QuickReplyManager::get_next_yet_unsent_message_id(Shortcut *s) const { return get_next_message_id(s, MessageType::YetUnsent); } MessageId QuickReplyManager::get_next_local_message_id(Shortcut *s) const { return get_next_message_id(s, MessageType::Local); } QuickReplyManager::QuickReplyMessage *QuickReplyManager::add_local_message( Shortcut *s, MessageId reply_to_message_id, unique_ptr &&content, bool invert_media, UserId via_bot_user_id, bool hide_via_bot, bool disable_web_page_preview, string &&send_emoji) { CHECK(s != nullptr); auto message = make_unique(); auto *m = message.get(); m->shortcut_id = s->shortcut_id_; m->message_id = get_next_yet_unsent_message_id(s); m->reply_to_message_id = reply_to_message_id; m->send_emoji = std::move(send_emoji); m->via_bot_user_id = via_bot_user_id; m->hide_via_bot = hide_via_bot; m->invert_media = invert_media; m->disable_web_page_preview = disable_web_page_preview; m->content = std::move(content); do { m->random_id = Random::secure_int64(); } while (m->random_id == 0); s->messages_.push_back(std::move(message)); s->local_total_count_++; return m; } vector QuickReplyManager::get_shortcut_ids() const { return transform(shortcuts_.shortcuts_, [](const unique_ptr &shortcut) { return shortcut->shortcut_id_; }); } vector QuickReplyManager::get_server_shortcut_ids() const { vector shortcut_ids; for (auto &shortcut : shortcuts_.shortcuts_) { if (shortcut->shortcut_id_.is_server()) { shortcut_ids.push_back(shortcut->shortcut_id_); } } return shortcut_ids; } void QuickReplyManager::sort_quick_reply_messages(vector> &messages) { std::sort(messages.begin(), messages.end(), [](const unique_ptr &lhs, const unique_ptr &rhs) { return lhs->message_id < rhs->message_id; }); } QuickReplyManager::QuickReplyMessageUniqueId QuickReplyManager::get_quick_reply_unique_id(const QuickReplyMessage *m) { return QuickReplyMessageUniqueId(m->message_id, m->edit_date); } vector QuickReplyManager::get_quick_reply_unique_ids( const vector> &messages) { return transform( messages, [](const unique_ptr &message) { return get_quick_reply_unique_id(message.get()); }); } vector QuickReplyManager::get_server_quick_reply_unique_ids( const vector> &messages) { auto message_ids = get_quick_reply_unique_ids(messages); td::remove_if(message_ids, [](const QuickReplyMessageUniqueId &message_id) { return !message_id.first.is_server(); }); return message_ids; } void QuickReplyManager::update_shortcut_from(Shortcut *new_shortcut, Shortcut *old_shortcut, bool is_partial, bool *is_shortcut_changed, bool *are_messages_changed) { CHECK(old_shortcut != nullptr); CHECK(new_shortcut != nullptr); CHECK(old_shortcut->shortcut_id_ == new_shortcut->shortcut_id_); CHECK(!old_shortcut->messages_.empty()); CHECK(!new_shortcut->messages_.empty()); auto old_unique_id = get_quick_reply_unique_id(old_shortcut->messages_[0].get()); auto old_message_count = get_shortcut_message_count(old_shortcut); if (is_partial) { // only the first server message is known // delete all definitely deleted server messages and insert the new message in the correct place auto old_message_ids = get_quick_reply_unique_ids(old_shortcut->messages_); auto new_first_message_id = new_shortcut->messages_[0]->message_id; auto it = old_shortcut->messages_.begin(); while (it != old_shortcut->messages_.end() && (*it)->message_id < new_first_message_id) { if ((*it)->message_id.is_server()) { delete_message_files(old_shortcut->shortcut_id_, it->get()); it = old_shortcut->messages_.erase(it); } else { ++it; } } if (it == old_shortcut->messages_.end() || (*it)->message_id != new_first_message_id) { change_message_files({old_shortcut->shortcut_id_, new_first_message_id}, new_shortcut->messages_[0].get(), {}); old_shortcut->messages_.insert(it, std::move(new_shortcut->messages_[0])); } else { update_quick_reply_message(old_shortcut->shortcut_id_, *it, std::move(new_shortcut->messages_[0])); } new_shortcut->messages_ = std::move(old_shortcut->messages_); *are_messages_changed = (old_message_ids != get_quick_reply_unique_ids(new_shortcut->messages_)); int32 server_total_count = 0; for (const auto &message : new_shortcut->messages_) { if (message->message_id.is_server()) { server_total_count++; } } if (server_total_count > new_shortcut->server_total_count_) { new_shortcut->server_total_count_ = server_total_count; } } else { auto old_server_message_ids = get_server_quick_reply_unique_ids(old_shortcut->messages_); auto new_server_message_ids = get_server_quick_reply_unique_ids(new_shortcut->messages_); CHECK(static_cast(new_shortcut->server_total_count_) == new_server_message_ids.size()); if (old_server_message_ids == new_server_message_ids) { *are_messages_changed = false; new_shortcut->messages_ = std::move(old_shortcut->messages_); } else { *are_messages_changed = true; for (auto &old_message : old_shortcut->messages_) { CHECK(old_message != nullptr); if (!old_message->message_id.is_server()) { new_shortcut->messages_.push_back(std::move(old_message)); } else { bool is_deleted = true; for (auto &new_message : new_shortcut->messages_) { if (new_message->message_id == old_message->message_id) { update_quick_reply_message(old_shortcut->shortcut_id_, old_message, std::move(new_message)); new_message = std::move(old_message); is_deleted = false; break; } } if (is_deleted) { delete_message_files(old_shortcut->shortcut_id_, old_message.get()); } } } sort_quick_reply_messages(new_shortcut->messages_); } } new_shortcut->local_total_count_ = old_shortcut->local_total_count_; *is_shortcut_changed = old_unique_id != get_quick_reply_unique_id(new_shortcut->messages_[0].get()) || new_shortcut->name_ != old_shortcut->name_ || old_message_count != get_shortcut_message_count(new_shortcut); } string QuickReplyManager::get_quick_reply_shortcuts_database_key() { return "quick_reply_shortcuts"; } void QuickReplyManager::save_quick_reply_shortcuts() { CHECK(shortcuts_.are_inited_); G()->td_db()->get_binlog_pmc()->set(get_quick_reply_shortcuts_database_key(), log_event_store(shortcuts_).as_slice().str()); } void QuickReplyManager::load_quick_reply_shortcuts() { CHECK(!td_->auth_manager_->is_bot()); if (shortcuts_.are_loaded_from_database_) { return; } shortcuts_.are_loaded_from_database_ = true; CHECK(shortcuts_.load_queries_.empty()); auto shortcuts_str = G()->td_db()->get_binlog_pmc()->get(get_quick_reply_shortcuts_database_key()); auto status = log_event_parse(shortcuts_, shortcuts_str); if (status.is_error()) { LOG(ERROR) << "Can't load quick replies: " << status; G()->td_db()->get_binlog_pmc()->erase(get_quick_reply_shortcuts_database_key()); shortcuts_.shortcuts_.clear(); return; } Dependencies dependencies; for (const auto &shortcut : shortcuts_.shortcuts_) { for (const auto &message : shortcut->messages_) { add_quick_reply_message_dependencies(dependencies, message.get()); } } if (!dependencies.resolve_force(td_, "load_quick_reply_shortcuts")) { shortcuts_.shortcuts_.clear(); return; } shortcuts_.are_inited_ = true; for (auto &shortcut : shortcuts_.shortcuts_) { if (shortcut->shortcut_id_.get() >= next_local_shortcut_id_) { next_local_shortcut_id_ = shortcut->shortcut_id_.get() + 1; } for (auto &message : shortcut->messages_) { if (message->shortcut_id != shortcut->shortcut_id_) { LOG(ERROR) << "Receive quick reply " << message->message_id << " in " << message->shortcut_id << " instead of " << shortcut->shortcut_id_; message->shortcut_id = shortcut->shortcut_id_; } change_message_files({shortcut->shortcut_id_, message->message_id}, message.get(), {}); if (message->message_id.is_server()) { if (need_reget_message_content(message->content.get()) || (message->legacy_layer != 0 && message->legacy_layer < MTPROTO_LAYER)) { reload_quick_reply_message(shortcut->shortcut_id_, message->message_id, Promise()); } if (message->edited_content != nullptr) { message->edit_generation = ++current_message_edit_generation_; do_send_message(message.get()); } } else if (message->message_id.is_yet_unsent()) { do_send_message(message.get()); } } send_update_quick_reply_shortcut(shortcut.get(), "load_quick_reply_shortcuts"); send_update_quick_reply_shortcut_messages(shortcut.get(), "load_quick_reply_shortcuts"); } send_update_quick_reply_shortcuts(); reload_quick_reply_shortcuts(); } td_api::object_ptr QuickReplyManager::get_update_quick_reply_shortcut_object( const Shortcut *s, const char *source) const { return td_api::make_object(get_quick_reply_shortcut_object(s, source)); } void QuickReplyManager::send_update_quick_reply_shortcut(const Shortcut *s, const char *source) { send_closure(G()->td(), &Td::send_update, get_update_quick_reply_shortcut_object(s, source)); } td_api::object_ptr QuickReplyManager::get_update_quick_reply_shortcut_deleted_object(const Shortcut *s) const { CHECK(s != nullptr); return td_api::make_object(s->shortcut_id_.get()); } void QuickReplyManager::send_update_quick_reply_shortcut_deleted(const Shortcut *s) { send_closure(G()->td(), &Td::send_update, get_update_quick_reply_shortcut_deleted_object(s)); } td_api::object_ptr QuickReplyManager::get_update_quick_reply_shortcuts_object() const { CHECK(shortcuts_.are_inited_); return td_api::make_object(transform( shortcuts_.shortcuts_, [](const unique_ptr &shortcut) { return shortcut->shortcut_id_.get(); })); } void QuickReplyManager::send_update_quick_reply_shortcuts() { send_closure(G()->td(), &Td::send_update, get_update_quick_reply_shortcuts_object()); } td_api::object_ptr QuickReplyManager::get_update_quick_reply_shortcut_messages_object(const Shortcut *s, const char *source) const { CHECK(s != nullptr); auto messages = transform(s->messages_, [this, source](const unique_ptr &message) { return get_quick_reply_message_object(message.get(), source); }); return td_api::make_object(s->shortcut_id_.get(), std::move(messages)); } void QuickReplyManager::send_update_quick_reply_shortcut_messages(const Shortcut *s, const char *source) { if (have_all_shortcut_messages(s)) { send_closure(G()->td(), &Td::send_update, get_update_quick_reply_shortcut_messages_object(s, source)); } } vector QuickReplyManager::get_message_file_ids(const QuickReplyMessage *m) const { if (m == nullptr) { return {}; } if (m->edited_content != nullptr) { auto file_ids = get_message_content_file_ids(m->edited_content.get(), td_); if (!file_ids.empty()) { for (auto file_id : get_message_content_file_ids(m->content.get(), td_)) { if (!td::contains(file_ids, file_id)) { file_ids.push_back(file_id); } } } } return get_message_content_file_ids(m->content.get(), td_); } void QuickReplyManager::delete_message_files(QuickReplyShortcutId shortcut_id, const QuickReplyMessage *m) const { CHECK(m != nullptr); auto file_ids = get_message_file_ids(m); if (file_ids.empty()) { return; } for (auto file_id : file_ids) { send_closure(G()->file_manager(), &FileManager::delete_file, file_id, Promise(), "delete_message_files"); } auto it = message_full_id_to_file_source_id_.find({shortcut_id, m->message_id}); if (it != message_full_id_to_file_source_id_.end()) { td_->file_manager_->change_files_source(it->second, file_ids, {}); } } void QuickReplyManager::change_message_files(QuickReplyMessageFullId message_full_id, const QuickReplyMessage *m, const vector &old_file_ids) { CHECK(m != nullptr); auto new_file_ids = get_message_file_ids(m); if (new_file_ids == old_file_ids) { return; } LOG(INFO) << "Change files of " << message_full_id << " from " << old_file_ids << " to " << new_file_ids; for (auto file_id : old_file_ids) { if (!td::contains(new_file_ids, file_id)) { send_closure(G()->file_manager(), &FileManager::delete_file, file_id, Promise(), "change_message_files"); } } auto file_source_id = get_quick_reply_message_file_source_id(message_full_id); if (file_source_id.is_valid()) { td_->file_manager_->change_files_source(file_source_id, old_file_ids, new_file_ids); } } FileSourceId QuickReplyManager::get_quick_reply_message_file_source_id(QuickReplyMessageFullId message_full_id) { if (td_->auth_manager_->is_bot()) { return FileSourceId(); } if (!message_full_id.is_server()) { return FileSourceId(); } auto &file_source_id = message_full_id_to_file_source_id_[message_full_id]; if (!file_source_id.is_valid()) { file_source_id = td_->file_reference_manager_->create_quick_reply_message_file_source(message_full_id); } return file_source_id; } void QuickReplyManager::get_current_state(vector> &updates) const { if (td_->auth_manager_->is_bot()) { return; } if (shortcuts_.are_inited_) { for (auto &shortcut : shortcuts_.shortcuts_) { updates.push_back(get_update_quick_reply_shortcut_object(shortcut.get(), "get_current_state")); if (have_all_shortcut_messages(shortcut.get())) { updates.push_back(get_update_quick_reply_shortcut_messages_object(shortcut.get(), "get_current_state")); } } updates.push_back(get_update_quick_reply_shortcuts_object()); } } } // namespace td