From 74b62ccbf40745875d391fe9177f9f264e682677 Mon Sep 17 00:00:00 2001 From: levlam Date: Wed, 13 Mar 2024 17:44:00 +0300 Subject: [PATCH] Add td_api::sendBusinessMessageAlbum. --- td/generate/scheme/td_api.tl | 21 ++- td/telegram/BusinessConnectionManager.cpp | 185 +++++++++++++++++++++- td/telegram/BusinessConnectionManager.h | 22 +++ td/telegram/Td.cpp | 9 ++ td/telegram/Td.h | 2 + td/telegram/cli.cpp | 23 ++- 6 files changed, 244 insertions(+), 18 deletions(-) diff --git a/td/generate/scheme/td_api.tl b/td/generate/scheme/td_api.tl index 69561a926..54cdcb467 100644 --- a/td/generate/scheme/td_api.tl +++ b/td/generate/scheme/td_api.tl @@ -7673,7 +7673,8 @@ setChatMessageSender chat_id:int53 message_sender_id:MessageSender = Ok; //@input_message_content The content of the message to be sent sendMessage chat_id:int53 message_thread_id:int53 reply_to:InputMessageReplyTo options:messageSendOptions reply_markup:ReplyMarkup input_message_content:InputMessageContent = Message; -//@description Sends 2-10 messages grouped together into an album. Currently, only audio, document, photo and video messages can be grouped into an album. Documents and audio files can be only grouped in an album with messages of the same type. Returns sent messages +//@description Sends 2-10 messages grouped together into an album. Currently, only audio, document, photo and video messages can be grouped into an album. +//-Documents and audio files can be only grouped in an album with messages of the same type. Returns sent messages //@chat_id Target chat //@message_thread_id If not 0, the message thread identifier in which the messages will be sent //@reply_to Information about the message or story to be replied; pass null if none @@ -7818,16 +7819,26 @@ editInlineMessageReplyMarkup inline_message_id:string reply_markup:ReplyMarkup = editMessageSchedulingState chat_id:int53 message_id:int53 scheduling_state:MessageSchedulingState = Ok; -//@description Sends a business message; for bots only. Returns the message after it was sent +//@description Sends a message on behalf of a business account; for bots only. Returns the message after it was sent //@business_connection_id Unique identifier of business connection on behalf of which to send the request //@chat_id Target chat -//@reply_to Information about the message; pass null if none +//@reply_to Information about the message to be replied; pass null if none //@disable_notification Pass true to disable notification for the message -//@protect_content Pass true if the content of the message must be protected from forwarding and saving; for bots only -//@reply_markup Markup for replying to the message; pass null if none; for bots only +//@protect_content Pass true if the content of the message must be protected from forwarding and saving +//@reply_markup Markup for replying to the message; pass null if none //@input_message_content The content of the message to be sent sendBusinessMessage business_connection_id:string chat_id:int53 reply_to:InputMessageReplyTo disable_notification:Bool protect_content:Bool reply_markup:ReplyMarkup input_message_content:InputMessageContent = Message; +//@description Sends 2-10 messages grouped together into an album on behalf of a business account; for bots only. Currently, only audio, document, photo and video messages can be grouped into an album. +//-Documents and audio files can be only grouped in an album with messages of the same type. Returns sent messages +//@business_connection_id Unique identifier of business connection on behalf of which to send the request +//@chat_id Target chat +//@reply_to Information about the message to be replied; pass null if none +//@disable_notification Pass true to disable notification for the message +//@protect_content Pass true if the content of the message must be protected from forwarding and saving +//@input_message_contents Contents of messages to be sent. At most 10 messages can be added to an album +sendBusinessMessageAlbum business_connection_id:string chat_id:int53 reply_to:InputMessageReplyTo disable_notification:Bool protect_content:Bool input_message_contents:vector = Messages; + //@description Checks validness of a name for a quick reply shortcut. Can be called synchronously @name The name of the shortcut; 1-32 characters checkQuickReplyShortcutName name:string = Ok; diff --git a/td/telegram/BusinessConnectionManager.cpp b/td/telegram/BusinessConnectionManager.cpp index 7a5b5dafb..c29b06ba4 100644 --- a/td/telegram/BusinessConnectionManager.cpp +++ b/td/telegram/BusinessConnectionManager.cpp @@ -15,6 +15,7 @@ #include "td/telegram/files/FileType.h" #include "td/telegram/Global.h" #include "td/telegram/MessageContent.h" +#include "td/telegram/MessageContentType.h" #include "td/telegram/MessageCopyOptions.h" #include "td/telegram/MessageEntity.h" #include "td/telegram/MessageId.h" @@ -103,7 +104,6 @@ struct BusinessConnectionManager::PendingMessage { MessageSelfDestructType ttl_; unique_ptr content_; unique_ptr reply_markup_; - int64 media_album_id_ = 0; int64 random_id_ = 0; bool noforwards_ = false; bool disable_notification_ = false; @@ -255,6 +255,66 @@ class BusinessConnectionManager::SendBusinessMediaQuery final : public Td::Resul } }; +class BusinessConnectionManager::SendBusinessMultiMediaQuery final : public Td::ResultHandler { + Promise> promise_; + vector> messages_; + + public: + explicit SendBusinessMultiMediaQuery(Promise> &&promise) + : promise_(std::move(promise)) { + } + + void send(vector> &&messages, + vector> &&input_single_media) { + CHECK(!messages.empty()); + messages_ = std::move(messages); + + int32 flags = 0; + if (messages_[0]->disable_notification_) { + flags |= telegram_api::messages_sendMultiMedia::SILENT_MASK; + } + if (messages_[0]->noforwards_) { + flags |= telegram_api::messages_sendMultiMedia::NOFORWARDS_MASK; + } + if (messages_[0]->invert_media_) { + flags |= telegram_api::messages_sendMultiMedia::INVERT_MEDIA_MASK; + } + + auto input_peer = td_->dialog_manager_->get_input_peer(messages_[0]->dialog_id_, AccessRights::Know); + CHECK(input_peer != nullptr); + + auto reply_to = messages_[0]->input_reply_to_.get_input_reply_to(td_, MessageId()); + if (reply_to != nullptr) { + flags |= telegram_api::messages_sendMultiMedia::REPLY_TO_MASK; + } + + send_query(G()->net_query_creator().create_with_prefix( + messages_[0]->business_connection_id_.get_invoke_prefix(), + telegram_api::messages_sendMultiMedia(flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, + false /*ignored*/, false /*ignored*/, false /*ignored*/, + std::move(input_peer), std::move(reply_to), std::move(input_single_media), + 0, nullptr, nullptr), + td_->business_connection_manager_->get_business_connection_dc_id(messages_[0]->business_connection_id_), + {{messages_[0]->dialog_id_}})); + } + + 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 SendBusinessMultiMediaQuery: " << to_string(ptr); + promise_.set_value(nullptr); // TODO + } + + void on_error(Status status) final { + LOG(INFO) << "Receive error for SendBusinessMultiMediaQuery: " << status; + promise_.set_error(std::move(status)); + } +}; + class BusinessConnectionManager::UploadBusinessMediaQuery final : public Td::ResultHandler { Promise promise_; unique_ptr message_; @@ -661,6 +721,10 @@ void BusinessConnectionManager::do_send_message(unique_ptr &&mes td_->create_handler(std::move(promise))->send(std::move(message), std::move(input_media)); return; } + if (content_type == MessageContentType::Game || content_type == MessageContentType::Poll || + content_type == MessageContentType::Story) { + return promise.set_error(Status::Error(400, "Message has no file")); + } upload_media(std::move(message), PromiseCreator::lambda([actor_id = actor_id(this), promise = std::move(promise)]( Result &&result) mutable { if (result.is_error()) { @@ -691,17 +755,23 @@ FileId BusinessConnectionManager::get_message_thumbnail_file_id(const unique_ptr void BusinessConnectionManager::upload_media(unique_ptr &&message, Promise &&promise, vector bad_parts) { - auto content_type = message->content_->get_type(); - if (content_type == MessageContentType::Game || content_type == MessageContentType::Poll || - content_type == MessageContentType::Story) { - return promise.set_error(Status::Error(400, "Message has no file")); + auto file_id = get_message_file_id(message); + FileView file_view = td_->file_manager_->get_file_view(file_id); + if (file_view.is_encrypted()) { + return promise.set_error(Status::Error(400, "Can't use encrypted file")); + } + if (file_view.has_remote_location() && file_view.main_remote_location().is_web()) { + return promise.set_error(Status::Error(400, "Can't use a web file")); } BeingUploadedMedia media; media.message_ = std::move(message); media.promise_ = std::move(promise); - auto file_id = get_message_file_id(media.message_); + if (!file_view.has_remote_location() && file_view.has_url()) { + return do_upload_media(std::move(media), nullptr); + } + 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::move(media)).second; @@ -784,8 +854,7 @@ void BusinessConnectionManager::do_upload_media(BeingUploadedMedia &&being_uploa file_id, thumbnail_file_id, message->ttl_, message->send_emoji_, true); CHECK(input_media != nullptr); auto input_media_id = input_media->get_id(); - if (!have_input_file || input_media_id == telegram_api::inputMediaDocument::ID || - input_media_id == telegram_api::inputMediaPhoto::ID) { + if (input_media_id == telegram_api::inputMediaDocument::ID || input_media_id == telegram_api::inputMediaPhoto::ID) { // can use input media directly UploadMediaResult result; result.message_ = std::move(being_uploaded_media.message_); @@ -880,4 +949,104 @@ void BusinessConnectionManager::complete_upload_media(unique_ptr promise.set_value(std::move(result)); } +void BusinessConnectionManager::send_message_album( + BusinessConnectionId business_connection_id, DialogId dialog_id, + td_api::object_ptr &&reply_to, bool disable_notification, bool protect_content, + vector> &&input_message_contents, + Promise> &&promise) { + if (input_message_contents.size() > MAX_GROUPED_MESSAGES) { + return promise.set_error(Status::Error(400, "Too many messages to send as an album")); + } + if (input_message_contents.empty()) { + return promise.set_error(Status::Error(400, "There are no messages to send")); + } + + TRY_STATUS_PROMISE(promise, check_business_connection(business_connection_id, dialog_id)); + + vector message_contents; + std::unordered_set message_content_types; + for (auto &input_message_content : input_message_contents) { + TRY_RESULT_PROMISE(promise, message_content, + process_input_message_content(dialog_id, std::move(input_message_content))); + auto message_content_type = message_content.content->get_type(); + if (!is_allowed_media_group_content(message_content_type)) { + return promise.set_error(Status::Error(400, "Invalid message content type")); + } + message_content_types.insert(message_content_type); + + message_contents.push_back(std::move(message_content)); + } + if (message_content_types.size() > 1) { + for (auto message_content_type : message_content_types) { + if (is_homogenous_media_group_content(message_content_type)) { + return promise.set_error( + Status::Error(400, PSLICE() << message_content_type << " can't be mixed with other media types")); + } + } + } + auto input_reply_to = create_business_message_input_reply_to(std::move(reply_to)); + + auto request_id = ++current_media_group_send_request_id_; + auto &request = media_group_send_requests_[request_id]; + request.upload_results_.resize(message_contents.size()); + request.promise_ = std::move(promise); + + for (size_t media_pos = 0; media_pos < message_contents.size(); media_pos++) { + auto &message_content = message_contents[media_pos]; + auto message = + create_business_message_to_send(business_connection_id, dialog_id, input_reply_to.clone(), disable_notification, + protect_content, nullptr, std::move(message_content)); + upload_media(std::move(message), PromiseCreator::lambda([actor_id = actor_id(this), request_id, + media_pos](Result &&result) mutable { + send_closure(actor_id, &BusinessConnectionManager::on_upload_message_album_media, request_id, + media_pos, std::move(result)); + })); + } +} + +void BusinessConnectionManager::on_upload_message_album_media(int64 request_id, size_t media_pos, + Result &&result) { + auto it = media_group_send_requests_.find(request_id); + CHECK(it != media_group_send_requests_.end()); + auto &request = it->second; + + request.upload_results_[media_pos] = std::move(result); + request.finished_count_++; + + LOG(INFO) << "Receive uploaded media " << media_pos << " for request " << request_id; + if (request.finished_count_ != request.upload_results_.size()) { + return; + } + + auto upload_results = std::move(request.upload_results_); + auto promise = std::move(request.promise_); + media_group_send_requests_.erase(it); + + TRY_STATUS_PROMISE(promise, G()->close_status()); + for (auto &r_upload_result : upload_results) { + if (r_upload_result.is_error()) { + return promise.set_error(r_upload_result.move_as_error()); + } + } + vector> messages; + vector> input_single_media; + for (auto &r_upload_result : upload_results) { + auto upload_result = r_upload_result.move_as_ok(); + auto message = std::move(upload_result.message_); + int32 flags = 0; + const FormattedText *caption = get_message_content_text(message->content_.get()); + auto entities = get_input_message_entities(td_->contacts_manager_.get(), caption, "on_upload_message_album_media"); + if (!entities.empty()) { + flags |= telegram_api::inputSingleMedia::ENTITIES_MASK; + } + input_single_media.push_back(telegram_api::make_object( + flags, std::move(upload_result.input_media_), message->random_id_, + caption == nullptr ? string() : caption->text, std::move(entities))); + messages.push_back(std::move(message)); + } + + td_->create_handler(std::move(promise)) + ->send(std::move(messages), std::move(input_single_media)); +} + } // namespace td diff --git a/td/telegram/BusinessConnectionManager.h b/td/telegram/BusinessConnectionManager.h index a8e37f1ac..8e8501800 100644 --- a/td/telegram/BusinessConnectionManager.h +++ b/td/telegram/BusinessConnectionManager.h @@ -63,11 +63,20 @@ class BusinessConnectionManager final : public Actor { td_api::object_ptr &&input_message_content, Promise> &&promise); + void send_message_album(BusinessConnectionId business_connection_id, DialogId dialog_id, + td_api::object_ptr &&reply_to, bool disable_notification, + bool protect_content, + vector> &&input_message_contents, + Promise> &&promise); + private: + static constexpr size_t MAX_GROUPED_MESSAGES = 10; // server side limit + struct BusinessConnection; struct PendingMessage; class SendBusinessMessageQuery; class SendBusinessMediaQuery; + class SendBusinessMultiMediaQuery; class UploadBusinessMediaQuery; class UploadMediaCallback; class UploadThumbnailCallback; @@ -83,6 +92,12 @@ class BusinessConnectionManager final : public Actor { Promise promise_; }; + struct MediaGroupSendRequest { + size_t finished_count_ = 0; + vector> upload_results_; + Promise> promise_; + }; + void tear_down() final; void on_get_business_connection(const BusinessConnectionId &connection_id, @@ -127,12 +142,19 @@ class BusinessConnectionManager final : public Actor { telegram_api::object_ptr &&media, Promise &&promise); + int64 generate_new_media_album_id(); + + void on_upload_message_album_media(int64 request_id, size_t media_pos, Result &&result); + WaitFreeHashMap, BusinessConnectionIdHash> business_connections_; FlatHashMap>>, BusinessConnectionIdHash> get_business_connection_queries_; + int64 current_media_group_send_request_id_ = 0; + FlatHashMap media_group_send_requests_; + std::shared_ptr upload_media_callback_; std::shared_ptr upload_thumbnail_callback_; diff --git a/td/telegram/Td.cpp b/td/telegram/Td.cpp index b079535e9..7ebd98a5a 100644 --- a/td/telegram/Td.cpp +++ b/td/telegram/Td.cpp @@ -5798,6 +5798,15 @@ void Td::on_request(uint64 id, td_api::sendBusinessMessage &request) { std::move(request.reply_markup_), std::move(request.input_message_content_), std::move(promise)); } +void Td::on_request(uint64 id, td_api::sendBusinessMessageAlbum &request) { + CHECK_IS_BOT(); + CREATE_REQUEST_PROMISE(); + business_connection_manager_->send_message_album(BusinessConnectionId(std::move(request.business_connection_id_)), + DialogId(request.chat_id_), std::move(request.reply_to_), + request.disable_notification_, request.protect_content_, + std::move(request.input_message_contents_), std::move(promise)); +} + void Td::on_request(uint64 id, const td_api::loadQuickReplyShortcuts &request) { CHECK_IS_USER(); CREATE_OK_REQUEST_PROMISE(); diff --git a/td/telegram/Td.h b/td/telegram/Td.h index 8c1d23181..ceae25e79 100644 --- a/td/telegram/Td.h +++ b/td/telegram/Td.h @@ -878,6 +878,8 @@ class Td final : public Actor { void on_request(uint64 id, td_api::sendBusinessMessage &request); + void on_request(uint64 id, td_api::sendBusinessMessageAlbum &request); + void on_request(uint64 id, const td_api::loadQuickReplyShortcuts &request); void on_request(uint64 id, const td_api::setQuickReplyShortcutName &request); diff --git a/td/telegram/cli.cpp b/td/telegram/cli.cpp index f0fcb6c44..e31fad6c2 100644 --- a/td/telegram/cli.cpp +++ b/td/telegram/cli.cpp @@ -4773,9 +4773,15 @@ class CliClient final : public Actor { rand_bool() ? get_message_self_destruct_type() : nullptr, has_spoiler_ && rand_bool()); return content; }); - send_request(td_api::make_object( - chat_id, message_thread_id_, get_input_message_reply_to(), default_message_send_options(), - std::move(input_message_contents))); + if (!business_connection_id_.empty()) { + send_request(td_api::make_object( + business_connection_id_, chat_id, get_input_message_reply_to(), rand_bool(), rand_bool(), + std::move(input_message_contents))); + } else { + send_request(td_api::make_object( + chat_id, message_thread_id_, get_input_message_reply_to(), default_message_send_options(), + std::move(input_message_contents))); + } } else if (op == "smad") { ChatId chat_id; get_args(args, chat_id, args); @@ -4784,8 +4790,15 @@ class CliClient final : public Actor { td_api::make_object(as_input_file(document), nullptr, true, as_caption("")); return content; }); - send_request(td_api::make_object( - chat_id, message_thread_id_, nullptr, default_message_send_options(), std::move(input_message_contents))); + if (!business_connection_id_.empty()) { + send_request(td_api::make_object( + business_connection_id_, chat_id, get_input_message_reply_to(), rand_bool(), rand_bool(), + std::move(input_message_contents))); + } else { + send_request(td_api::make_object( + chat_id, message_thread_id_, get_input_message_reply_to(), default_message_send_options(), + std::move(input_message_contents))); + } } else if (op == "gmft") { auto r_message_file_head = read_file_str(args, 2 << 10); if (r_message_file_head.is_error()) {