From c73ae5c781adf44d65a8a7c0f096456f6aca49f0 Mon Sep 17 00:00:00 2001 From: levlam Date: Thu, 25 May 2023 15:03:29 +0300 Subject: [PATCH] Add td_api::sendStory. --- td/generate/scheme/td_api.tl | 9 +- td/telegram/StoryManager.cpp | 186 +++++++++++++++++++++++++++++++++++ td/telegram/StoryManager.h | 35 +++++++ td/telegram/Td.cpp | 7 ++ td/telegram/Td.h | 2 + td/telegram/cli.cpp | 23 +++++ 6 files changed, 261 insertions(+), 1 deletion(-) diff --git a/td/generate/scheme/td_api.tl b/td/generate/scheme/td_api.tl index 47c3efda9..a98193069 100644 --- a/td/generate/scheme/td_api.tl +++ b/td/generate/scheme/td_api.tl @@ -4856,7 +4856,7 @@ inputStoryContentPhoto photo:InputFile added_sticker_file_ids:vector = In //@description A video story //@video Video to be sent. The video size must be 720x1280. The video must be stored in MPEG4 format, encoded by x265 codec and must be streamable //@added_sticker_file_ids File identifiers of the stickers added to the video, if applicable -//@duration Duration of the video, in seconds +//@duration Duration of the video, in seconds; 0-60 inputStoryContentVideo video:InputFile added_sticker_file_ids:vector duration:int32 = InputStoryContent; @@ -7162,6 +7162,13 @@ setPinnedChats chat_list:ChatList chat_ids:vector = Ok; readChatList chat_list:ChatList = Ok; +//@description Sends a new story. Returns a temporary story with identifier 0 +//@content Content of the story +//@caption Story caption; pass null to use an empty caption; 0-getOption("message_caption_length_max") characters +//@privacy_rules The privacy rules for the story +//@is_pinned Pass true to keep the story accessible after 24 hours +sendStory content:InputStoryContent caption:formattedText privacy_rules:userPrivacySettingRules is_pinned:Bool = Story; + //@description Returns the list of pinned stories of a given user. The stories are returned in a reverse chronological order (i.e., in order of decreasing story_id). //-For optimal performance, the number of returned stories is chosen by TDLib //@user_id User identifier diff --git a/td/telegram/StoryManager.cpp b/td/telegram/StoryManager.cpp index 37715c6d5..31a3ec2cf 100644 --- a/td/telegram/StoryManager.cpp +++ b/td/telegram/StoryManager.cpp @@ -16,10 +16,14 @@ #include "td/telegram/StoryContent.h" #include "td/telegram/StoryContentType.h" #include "td/telegram/Td.h" +#include "td/telegram/UpdatesManager.h" + +#include "tddb/td/db/binlog/BinlogHelper.h" #include "td/utils/algorithm.h" #include "td/utils/buffer.h" #include "td/utils/logging.h" +#include "td/utils/Random.h" #include "td/utils/Status.h" namespace td { @@ -124,7 +128,89 @@ class GetUserStoriesQuery final : public Td::ResultHandler { } }; +class StoryManager::SendStoryQuery final : public Td::ResultHandler { + unique_ptr pending_story_; + + public: + void send(unique_ptr pending_story, telegram_api::object_ptr input_file) { + pending_story_ = std::move(pending_story); + CHECK(pending_story_ != nullptr); + + const auto *story = pending_story_->story_.get(); + const StoryContent *content = story->content_.get(); + auto input_media = get_story_content_input_media(td_, content, std::move(input_file)); + CHECK(input_media != nullptr); + + const FormattedText &caption = story->caption_; + auto entities = get_input_message_entities(td_->contacts_manager_.get(), &caption, "SendStoryQuery"); + auto privacy_rules = story->privacy_rules_.get_input_privacy_rules(td_); + int32 flags = 0; + if (!caption.text.empty()) { + flags |= telegram_api::stories_sendStory::CAPTION_MASK; + } + if (!entities.empty()) { + flags |= telegram_api::stories_sendStory::ENTITIES_MASK; + } + if (pending_story_->story_->is_pinned_) { + flags |= telegram_api::stories_sendStory::PINNED_MASK; + } + + send_query(G()->net_query_creator().create( + telegram_api::stories_sendStory(flags, false /*ignored*/, false /*ignored*/, std::move(input_media), + caption.text, std::move(entities), std::move(privacy_rules), + pending_story_->random_id_, 86400), + {{pending_story_->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 SendStoryQuery: " << to_string(ptr); + td_->updates_manager_->on_get_updates(std::move(ptr), Promise()); + } + + void on_error(Status status) final { + LOG(INFO) << "Receive error for SendStoryQuery: " << status; + if (G()->close_flag() && G()->use_message_database()) { + // do not send error, message will be re-sent + return; + } + } +}; + +class StoryManager::UploadMediaCallback final : public FileManager::UploadCallback { + public: + void on_upload_ok(FileId file_id, telegram_api::object_ptr input_file) final { + send_closure_later(G()->story_manager(), &StoryManager::on_upload_story, 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()->story_manager(), &StoryManager::on_upload_story_error, file_id, std::move(error)); + } +}; + +StoryManager::PendingStory::PendingStory(DialogId dialog_id, StoryId story_id, uint64 log_event_id, + uint32 send_story_num, int64 random_id, unique_ptr &&story) + : dialog_id_(dialog_id) + , story_id_(story_id) + , log_event_id_(log_event_id) + , send_story_num_(send_story_num) + , random_id_(random_id) + , story_(std::move(story)) { +} + StoryManager::StoryManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) { + upload_media_callback_ = std::make_shared(); } StoryManager::~StoryManager() = default; @@ -429,4 +515,104 @@ void StoryManager::reload_story(StoryFullId story_full_id, Promise &&promi td_->create_handler(std::move(promise))->send(user_id, {story_full_id.get_story_id().get()}); } +void StoryManager::send_story(td_api::object_ptr &&input_story_content, + td_api::object_ptr &&input_caption, + td_api::object_ptr &&rules, bool is_pinned, + Promise> &&promise) { + if (input_story_content == nullptr) { + return promise.set_error(Status::Error(400, "Can't send story without content")); + } + bool is_bot = td_->auth_manager_->is_bot(); + DialogId dialog_id(td_->contacts_manager_->get_my_id()); + TRY_RESULT_PROMISE(promise, content, get_input_story_content(td_, std::move(input_story_content), dialog_id)); + TRY_RESULT_PROMISE(promise, caption, + get_formatted_text(td_, DialogId(), std::move(input_caption), is_bot, true, false, false)); + TRY_RESULT_PROMISE(promise, privacy_rules, + UserPrivacySettingRules::get_user_privacy_setting_rules(td_, std::move(rules))); + + auto story = make_unique(); + story->date_ = G()->unix_time(); + story->expire_date_ = std::numeric_limits::max(); + story->is_pinned_ = is_pinned; + story->privacy_rules_ = std::move(privacy_rules); + story->content_ = std::move(content); + story->caption_ = std::move(caption); + + int64 random_id; + do { + random_id = Random::secure_int64(); + } while (random_id == 0); + + // auto log_event_id = save_send_story_log_event(dialog_id, random_id, story.get()); + + do_send_story(dialog_id, StoryId(), 0 /*log_event_id*/, ++send_story_count_, random_id, std::move(story), {}, + std::move(promise)); +} + +void StoryManager::do_send_story(DialogId dialog_id, StoryId story_id, uint64 log_event_id, uint32 send_story_num, + int64 random_id, unique_ptr &&story, vector bad_parts, + Promise> &&promise) { + auto story_ptr = story.get(); + auto content = story_ptr->content_.get(); + CHECK(content != nullptr); + + FileId file_id = get_story_content_any_file_id(td_, content); + CHECK(file_id.is_valid()); + + LOG(INFO) << "Ask to upload file " << file_id << " with bad parts " << bad_parts; + auto pending_story = + td::make_unique(dialog_id, story_id, log_event_id, send_story_num, random_id, std::move(story)); + bool is_inserted = being_uploaded_files_.emplace(file_id, std::move(pending_story)).second; + CHECK(is_inserted); + // need to call resume_upload synchronously to make upload process consistent with being_uploaded_files_ + // and to send is_uploading_active == true in response + td_->file_manager_->resume_upload(file_id, std::move(bad_parts), upload_media_callback_, 1, send_story_num); + + promise.set_value(get_story_object({dialog_id, story_id}, story_ptr)); +} + +void StoryManager::on_upload_story(FileId file_id, telegram_api::object_ptr input_file) { + if (G()->close_flag()) { + return; + } + + LOG(INFO) << "File " << file_id << " has been uploaded"; + + auto it = being_uploaded_files_.find(file_id); + if (it == being_uploaded_files_.end()) { + // callback may be called just before the file upload was canceled + return; + } + CHECK(input_file != nullptr); + + auto pending_story = std::move(it->second); + + being_uploaded_files_.erase(it); + + td_->create_handler()->send(std::move(pending_story), std::move(input_file)); +} + +void StoryManager::on_upload_story_error(FileId file_id, Status status) { + if (G()->close_flag()) { + // do not fail upload if closing + return; + } + + LOG(INFO) << "File " << file_id << " has upload error " << status; + + auto it = being_uploaded_files_.find(file_id); + if (it == being_uploaded_files_.end()) { + // callback may be called just before the file upload was canceled + return; + } + + auto pending_story = std::move(it->second); + + if (pending_story->log_event_id_ != 0) { + binlog_erase(G()->td_db()->get_binlog(), pending_story->log_event_id_); + } + + being_uploaded_files_.erase(it); +} + } // namespace td diff --git a/td/telegram/StoryManager.h b/td/telegram/StoryManager.h index 4bb14b407..675e3b6c1 100644 --- a/td/telegram/StoryManager.h +++ b/td/telegram/StoryManager.h @@ -39,6 +39,11 @@ class StoryManager final : public Actor { StoryManager &operator=(StoryManager &&) = delete; ~StoryManager() final; + void send_story(td_api::object_ptr &&input_story_content, + td_api::object_ptr &&input_caption, + td_api::object_ptr &&rules, bool is_pinned, + Promise> &&promise); + void get_dialog_pinned_stories(DialogId owner_dialog_id, StoryId from_story_id, int32 limit, Promise> &&promise); @@ -71,6 +76,22 @@ class StoryManager final : public Actor { FormattedText caption_; }; + struct PendingStory { + DialogId dialog_id_; + StoryId story_id_; + uint64 log_event_id_ = 0; + uint32 send_story_num_ = 0; + int64 random_id_ = 0; + unique_ptr story_; + + PendingStory(DialogId dialog_id, StoryId story_id, uint64 log_event_id, uint32 send_story_num, int64 random_id, + unique_ptr &&story); + }; + + class UploadMediaCallback; + + class SendStoryQuery; + void tear_down() final; bool is_story_owned(DialogId owner_dialog_id) const; @@ -100,10 +121,24 @@ class StoryManager final : public Actor { static bool is_local_story_id(StoryId story_id); + void do_send_story(DialogId dialog_id, StoryId story_id, uint64 log_event_id, uint32 send_story_num, int64 random_id, + unique_ptr &&story, vector bad_parts, + Promise> &&promise); + + void on_upload_story(FileId file_id, telegram_api::object_ptr input_file); + + void on_upload_story_error(FileId file_id, Status status); + + std::shared_ptr upload_media_callback_; + WaitFreeHashMap story_full_id_to_file_source_id_; WaitFreeHashMap, StoryFullIdHash> stories_; + uint32 send_story_count_ = 0; + + FlatHashMap, FileIdHash> being_uploaded_files_; + Td *td_; ActorShared<> parent_; }; diff --git a/td/telegram/Td.cpp b/td/telegram/Td.cpp index a118c60a9..75654b136 100644 --- a/td/telegram/Td.cpp +++ b/td/telegram/Td.cpp @@ -5617,6 +5617,13 @@ void Td::on_request(uint64 id, td_api::editMessageSchedulingState &request) { std::move(request.scheduling_state_), std::move(promise)); } +void Td::on_request(uint64 id, td_api::sendStory &request) { + CHECK_IS_USER(); + CREATE_REQUEST_PROMISE(); + story_manager_->send_story(std::move(request.content_), std::move(request.caption_), + std::move(request.privacy_rules_), request.is_pinned_, std::move(promise)); +} + void Td::on_request(uint64 id, const td_api::getForumTopicDefaultIcons &request) { CREATE_REQUEST_PROMISE(); stickers_manager_->get_default_topic_icons(false, std::move(promise)); diff --git a/td/telegram/Td.h b/td/telegram/Td.h index 894263a9f..6ad771bdf 100644 --- a/td/telegram/Td.h +++ b/td/telegram/Td.h @@ -786,6 +786,8 @@ class Td final : public Actor { void on_request(uint64 id, td_api::editMessageSchedulingState &request); + void on_request(uint64 id, td_api::sendStory &request); + void on_request(uint64 id, const td_api::getForumTopicDefaultIcons &request); void on_request(uint64 id, td_api::createForumTopic &request); diff --git a/td/telegram/cli.cpp b/td/telegram/cli.cpp index 1727a1583..f3eac3b78 100644 --- a/td/telegram/cli.cpp +++ b/td/telegram/cli.cpp @@ -3911,6 +3911,29 @@ class CliClient final : public Actor { send_request(td_api::make_object(as_chat_list(op), as_chat_ids(args))); } else if (op == "rcl" || op == "rcla" || begins_with(op, "rcl-")) { send_request(td_api::make_object(as_chat_list(op))); + } else if (op == "ssp") { + string photo; + string caption; + string allow; + string ids; + string sticker_file_ids; + get_args(args, photo, caption, allow, ids, sticker_file_ids); + send_request( + td_api::make_object(td_api::make_object( + as_input_file(photo), to_integers(sticker_file_ids)), + as_caption(caption), as_user_privacy_setting_rules(allow, ids), true)); + } else if (op == "ssv") { + string video; + string caption; + string allow; + string ids; + int32 duration; + string sticker_file_ids; + get_args(args, video, caption, allow, ids, duration, sticker_file_ids); + send_request(td_api::make_object( + td_api::make_object(as_input_file(video), + to_integers(sticker_file_ids), duration), + as_caption(caption), as_user_privacy_setting_rules(allow, ids), false)); } else if (op == "gups") { UserId user_id; StoryId from_story_id;