From 3a6494dd65b8d4aae505ef4741b2163f0162cb89 Mon Sep 17 00:00:00 2001 From: levlam Date: Tue, 30 May 2023 14:41:36 +0300 Subject: [PATCH] Add td_api::editStory. --- td/generate/scheme/td_api.tl | 6 + td/telegram/ChainId.h | 5 + td/telegram/MessagesManager.cpp | 18 +-- td/telegram/StoryManager.cpp | 216 ++++++++++++++++++++++++++++++-- td/telegram/StoryManager.h | 19 +++ td/telegram/Td.cpp | 7 ++ td/telegram/Td.h | 2 + td/telegram/cli.cpp | 28 +++++ 8 files changed, 283 insertions(+), 18 deletions(-) diff --git a/td/generate/scheme/td_api.tl b/td/generate/scheme/td_api.tl index 896be5c50..0a865c3f7 100644 --- a/td/generate/scheme/td_api.tl +++ b/td/generate/scheme/td_api.tl @@ -7169,6 +7169,12 @@ readChatList chat_list:ChatList = Ok; //@is_pinned Pass true to keep the story accessible after expiration sendStory content:InputStoryContent caption:formattedText privacy_rules:userPrivacySettingRules is_pinned:Bool = Story; +//@description Changes content and caption of a previously sent story +//@story_id Identifier of the story to edit +//@content New content of the story; pass null to keep the current content +//@caption New story caption; pass null to keep the current caption +editStory story_id:int32 content:InputStoryContent caption:formattedText = Ok; + //@description Changes privacy rules of a previously sent story @story_id Identifier of the story @privacy_rules The new privacy rules for the story setStoryPrivacyRules story_id:int32 privacy_rules:userPrivacySettingRules = Ok; diff --git a/td/telegram/ChainId.h b/td/telegram/ChainId.h index 16a006000..749d9c4f2 100644 --- a/td/telegram/ChainId.h +++ b/td/telegram/ChainId.h @@ -13,6 +13,7 @@ #include "td/telegram/FullMessageId.h" #include "td/telegram/MessageContentType.h" #include "td/telegram/PollId.h" +#include "td/telegram/StoryFullId.h" #include "td/telegram/UserId.h" #include "td/utils/common.h" @@ -53,6 +54,10 @@ class ChainId { ChainId(UserId user_id) : ChainId(DialogId(user_id)) { } + ChainId(StoryFullId story_full_id) : ChainId(story_full_id.get_dialog_id()) { + id += static_cast(story_full_id.get_story_id().get()) << 10; + } + uint64 get() const { return id; } diff --git a/td/telegram/MessagesManager.cpp b/td/telegram/MessagesManager.cpp index 4927c0c42..2d7c62c60 100644 --- a/td/telegram/MessagesManager.cpp +++ b/td/telegram/MessagesManager.cpp @@ -3205,7 +3205,7 @@ class SendMessageQuery final : public Td::ResultHandler { void on_error(Status status) final { LOG(INFO) << "Receive error for SendMessage: " << status; if (G()->close_flag() && G()->use_message_database()) { - // do not send error, message will be re-sent + // do not send error, message will be re-sent after restart return; } td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "SendMessageQuery"); @@ -3256,7 +3256,7 @@ class StartBotQuery final : public Td::ResultHandler { void on_error(Status status) final { LOG(INFO) << "Receive error for StartBotQuery: " << status; if (G()->close_flag() && G()->use_message_database()) { - // do not send error, message should be re-sent + // do not send error, message should be re-sent after restart return; } td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "StartBotQuery"); @@ -3310,7 +3310,7 @@ class SendInlineBotResultQuery final : public Td::ResultHandler { void on_error(Status status) final { LOG(INFO) << "Receive error for SendInlineBotResultQuery: " << status; if (G()->close_flag() && G()->use_message_database()) { - // do not send error, message will be re-sent + // do not send error, message will be re-sent after restart return; } td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "SendInlineBotResultQuery"); @@ -3408,7 +3408,7 @@ class SendMultiMediaQuery final : public Td::ResultHandler { void on_error(Status status) final { LOG(INFO) << "Receive error for SendMultiMedia: " << status; if (G()->close_flag() && G()->use_message_database()) { - // do not send error, message will be re-sent + // do not send error, message will be re-sent after restart return; } if (!td_->auth_manager_->is_bot() && FileReferenceManager::is_file_reference_error(status)) { @@ -3508,7 +3508,7 @@ class SendMediaQuery final : public Td::ResultHandler { void on_error(Status status) final { LOG(INFO) << "Receive error for SendMedia: " << status; if (G()->close_flag() && G()->use_message_database()) { - // do not send error, message will be re-sent + // do not send error, message will be re-sent after restart return; } if (was_uploaded_) { @@ -3596,7 +3596,7 @@ class UploadMediaQuery final : public Td::ResultHandler { void on_error(Status status) final { LOG(INFO) << "Receive error for UploadMediaQuery for " << message_id_ << " in " << dialog_id_ << ": " << status; if (G()->close_flag() && G()->use_message_database()) { - // do not send error, message will be re-sent + // do not send error, message will be re-sent after restart return; } td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "UploadMediaQuery"); @@ -3904,7 +3904,7 @@ class ForwardMessagesQuery final : public Td::ResultHandler { void on_error(Status status) final { LOG(INFO) << "Receive error for forward messages: " << status; if (G()->close_flag() && G()->use_message_database()) { - // do not send error, messages should be re-sent + // do not send error, messages will be re-sent after restart return; } // no on_get_dialog_error call, because two dialogs are involved @@ -3959,7 +3959,7 @@ class SendScreenshotNotificationQuery final : public Td::ResultHandler { void on_error(Status status) final { LOG(INFO) << "Receive error for SendScreenshotNotificationQuery: " << status; if (G()->close_flag() && G()->use_message_database()) { - // do not send error, messages should be re-sent + // do not send error, messages will be re-sent after restart return; } td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "SendScreenshotNotificationQuery"); @@ -13823,7 +13823,7 @@ void MessagesManager::on_send_secret_message_error(int64 random_id, Status error auto file_id = get_message_content_upload_file_id(m->content.get()); if (file_id.is_valid()) { if (G()->close_flag() && G()->use_message_database()) { - // do not send error, message will be re-sent + // do not send error, message will be re-sent after restart return; } if (begins_with(error.message(), "FILE_PART_") && ends_with(error.message(), "_MISSING")) { diff --git a/td/telegram/StoryManager.cpp b/td/telegram/StoryManager.cpp index 36b716932..d637aa5e5 100644 --- a/td/telegram/StoryManager.cpp +++ b/td/telegram/StoryManager.cpp @@ -187,7 +187,7 @@ class ToggleStoryPinnedQuery final : public Td::ResultHandler { class StoryManager::SendStoryQuery final : public Td::ResultHandler { FileId file_id_; - unique_ptr pending_story_; + unique_ptr pending_story_; public: void send(FileId file_id, unique_ptr pending_story, @@ -239,7 +239,7 @@ class StoryManager::SendStoryQuery final : public Td::ResultHandler { LOG(INFO) << "Receive error for SendStoryQuery: " << status; if (G()->close_flag() && G()->use_message_database()) { - // do not send error, message will be re-sent + // do not send error, story will be re-sent after restart return; } @@ -253,6 +253,73 @@ class StoryManager::SendStoryQuery final : public Td::ResultHandler { } }; +class StoryManager::EditStoryQuery final : public Td::ResultHandler { + FileId file_id_; + unique_ptr pending_story_; + + public: + void send(FileId file_id, unique_ptr pending_story, + telegram_api::object_ptr input_file, const BeingEditedStory *edited_story) { + file_id_ = file_id; + pending_story_ = std::move(pending_story); + CHECK(pending_story_ != nullptr); + + int32 flags = 0; + + telegram_api::object_ptr input_media; + const StoryContent *content = edited_story->content_.get(); + if (content != nullptr) { + CHECK(input_file != nullptr); + input_media = get_story_content_input_media(td_, content, std::move(input_file)); + CHECK(input_media != nullptr); + flags |= telegram_api::stories_editStory::MEDIA_MASK; + } + vector> entities; + if (edited_story->edit_caption_) { + flags |= telegram_api::stories_editStory::CAPTION_MASK; + flags |= telegram_api::stories_editStory::ENTITIES_MASK; + + entities = get_input_message_entities(td_->contacts_manager_.get(), &edited_story->caption_, "EditStoryQuery"); + } + send_query(G()->net_query_creator().create( + telegram_api::stories_editStory(flags, pending_story_->story_id_.get(), std::move(input_media), + edited_story->caption_.text, std::move(entities), Auto()), + {{StoryFullId{pending_story_->dialog_id_, pending_story_->story_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 EditStoryQuery: " << to_string(ptr); + td_->updates_manager_->on_get_updates( + std::move(ptr), PromiseCreator::lambda([file_id = file_id_, pending_story = std::move(pending_story_)]( + Result &&result) mutable { + send_closure(G()->story_manager(), &StoryManager::on_story_edited, file_id, std::move(pending_story), + std::move(result)); + })); + } + + void on_error(Status status) final { + LOG(INFO) << "Receive error for EditStoryQuery: " << status; + + if (G()->close_flag() && G()->use_message_database()) { + // do not send error, story will be edited after restart + return; + } + + if (begins_with(status.message(), "FILE_PART_") && ends_with(status.message(), "_MISSING")) { + td_->story_manager_->on_send_story_file_part_missing(std::move(pending_story_), + to_integer(status.message().substr(10))); + return; + } + td_->story_manager_->on_story_edited(file_id_, std::move(pending_story_), std::move(status)); + } +}; + class StoryManager::UploadMediaCallback final : public FileManager::UploadCallback { public: void on_upload_ok(FileId file_id, telegram_api::object_ptr input_file) final { @@ -400,13 +467,27 @@ td_api::object_ptr StoryManager::get_story_object(StoryFullId sto privacy_rules = story->privacy_rules_.get_user_privacy_setting_rules_object(td_); } + auto *content = story->content_.get(); + auto *caption = &story->caption_; + if (is_owned && story_full_id.get_story_id().is_server()) { + auto it = being_edited_stories_.find(story_full_id); + if (it != being_edited_stories_.end()) { + if (it->second->content_ != nullptr) { + content = it->second->content_.get(); + } + if (it->second->edit_caption_) { + caption = &it->second->caption_; + } + } + } + CHECK(dialog_id.get_type() == DialogType::User); return td_api::make_object( story_full_id.get_story_id().get(), td_->contacts_manager_->get_user_id_object(dialog_id.get_user_id(), "get_story_object"), story->date_, story->is_pinned_, story->interaction_info_.get_story_interaction_info_object(td_), std::move(privacy_rules), - story->is_public_, story->is_for_close_friends_, get_story_content_object(td_, story->content_.get()), - get_formatted_text_object(story->caption_, true, get_story_content_duration(td_, story->content_.get()))); + story->is_public_, story->is_for_close_friends_, get_story_content_object(td_, content), + get_formatted_text_object(story->caption_, true, get_story_content_duration(td_, content))); } td_api::object_ptr StoryManager::get_stories_object(int32 total_count, @@ -682,7 +763,12 @@ void StoryManager::on_upload_story(FileId file_id, telegram_api::object_ptrcreate_handler()->send(file_id, std::move(pending_story), std::move(input_file)); + bool is_edit = pending_story->story_id_.is_server(); + if (is_edit) { + do_edit_story(file_id, std::move(pending_story), std::move(input_file)); + } else { + td_->create_handler()->send(file_id, std::move(pending_story), std::move(input_file)); + } } void StoryManager::on_upload_story_error(FileId file_id, Status status) { @@ -701,17 +787,129 @@ void StoryManager::on_upload_story_error(FileId file_id, Status status) { 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); + + bool is_edit = pending_story->story_id_.is_server(); + if (is_edit) { + on_story_edited(file_id, std::move(pending_story), std::move(status)); + } else { + if (pending_story->log_event_id_ != 0) { + binlog_erase(G()->td_db()->get_binlog(), pending_story->log_event_id_); + } + } } void StoryManager::on_send_story_file_part_missing(unique_ptr &&pending_story, int bad_part) { do_send_story(std::move(pending_story), {bad_part}); } +void StoryManager::edit_story(StoryId story_id, td_api::object_ptr &&input_story_content, + td_api::object_ptr &&input_caption, Promise &&promise) { + DialogId dialog_id(td_->contacts_manager_->get_my_id()); + StoryFullId story_full_id{dialog_id, story_id}; + const Story *story = get_story(story_full_id); + if (story == nullptr) { + return promise.set_error(Status::Error(400, "Story not found")); + } + if (!story_id.is_server()) { + return promise.set_error(Status::Error(400, "Story can't be edited")); + } + + bool is_bot = td_->auth_manager_->is_bot(); + unique_ptr content; + bool is_caption_edited = input_caption != nullptr; + FormattedText caption; + if (input_story_content != nullptr) { + TRY_RESULT_PROMISE_ASSIGN(promise, content, + get_input_story_content(td_, std::move(input_story_content), dialog_id)); + } + if (is_caption_edited) { + TRY_RESULT_PROMISE_ASSIGN( + promise, caption, get_formatted_text(td_, DialogId(), std::move(input_caption), is_bot, true, false, false)); + auto *current_caption = &story->caption_; + auto it = being_edited_stories_.find(story_full_id); + if (it != being_edited_stories_.end() && it->second->edit_caption_) { + current_caption = &it->second->caption_; + } + if (*current_caption == caption) { + is_caption_edited = false; + } + } + if (content == nullptr && !is_caption_edited) { + return promise.set_value(Unit()); + } + + auto &edited_story = being_edited_stories_[story_full_id]; + if (edited_story == nullptr) { + edited_story = make_unique(); + } + if (content != nullptr) { + edited_story->content_ = std::move(content); + story->edit_generation_++; + } + if (is_caption_edited) { + edited_story->caption_ = std::move(caption); + edited_story->edit_caption_ = true; + story->edit_generation_++; + } + edited_story->promises_.push_back(std::move(promise)); + + auto new_story = make_unique(); + new_story->content_ = dup_story_content(td_, edited_story->content_.get()); + + auto pending_story = td::make_unique(dialog_id, story_id, 0 /*log_event_id*/, + std::numeric_limits::max() - (++send_story_count_), + story->edit_generation_, std::move(new_story)); + + if (edited_story->content_ == nullptr) { + return do_edit_story(FileId(), std::move(pending_story), nullptr); + } + + do_send_story(std::move(pending_story), {}); +} + +void StoryManager::do_edit_story(FileId file_id, unique_ptr &&pending_story, + telegram_api::object_ptr input_file) { + StoryFullId story_full_id{pending_story->dialog_id_, pending_story->story_id_}; + const Story *story = get_story(story_full_id); + auto it = being_edited_stories_.find(story_full_id); + if (story == nullptr || story->edit_generation_ != pending_story->random_id_ || it == being_edited_stories_.end()) { + LOG(INFO) << "Skip outdated edit of " << story_full_id; + if (file_id.is_valid()) { + td_->file_manager_->cancel_upload(file_id); + } + return; + } + td_->create_handler()->send(file_id, std::move(pending_story), std::move(input_file), + it->second.get()); +} + +void StoryManager::on_story_edited(FileId file_id, unique_ptr pending_story, Result result) { + G()->ignore_result_if_closing(result); + + if (file_id.is_valid()) { + td_->file_manager_->delete_partial_remote_location(file_id); + } + + StoryFullId story_full_id{pending_story->dialog_id_, pending_story->story_id_}; + const Story *story = get_story(story_full_id); + auto it = being_edited_stories_.find(story_full_id); + if (story == nullptr || story->edit_generation_ != pending_story->random_id_ || it == being_edited_stories_.end()) { + LOG(INFO) << "Ignore outdated edit of " << story_full_id; + return; + } + if (pending_story->log_event_id_ != 0) { + binlog_erase(G()->td_db()->get_binlog(), pending_story->log_event_id_); + } + auto promises = std::move(it->second->promises_); + being_edited_stories_.erase(it); + if (result.is_ok()) { + set_promises(promises); + } else { + fail_promises(promises, result.move_as_error()); + } +} + void StoryManager::set_story_privacy_rules(StoryId story_id, td_api::object_ptr &&rules, Promise &&promise) { diff --git a/td/telegram/StoryManager.h b/td/telegram/StoryManager.h index 109a40376..9016ab14c 100644 --- a/td/telegram/StoryManager.h +++ b/td/telegram/StoryManager.h @@ -41,6 +41,14 @@ class StoryManager final : public Actor { UserPrivacySettingRules privacy_rules_; unique_ptr content_; FormattedText caption_; + mutable int64 edit_generation_ = 0; + }; + + struct BeingEditedStory { + unique_ptr content_; + FormattedText caption_; + bool edit_caption_ = false; + vector> promises_; }; struct PendingStory { @@ -71,6 +79,9 @@ class StoryManager final : public Actor { void on_send_story_file_part_missing(unique_ptr &&pending_story, int bad_part); + void edit_story(StoryId story_id, td_api::object_ptr &&input_story_content, + td_api::object_ptr &&input_caption, Promise &&promise); + void set_story_privacy_rules(StoryId story_id, td_api::object_ptr &&rules, Promise &&promise); @@ -99,6 +110,7 @@ class StoryManager final : public Actor { class UploadMediaCallback; class SendStoryQuery; + class EditStoryQuery; void tear_down() final; @@ -135,6 +147,11 @@ class StoryManager final : public Actor { void on_upload_story_error(FileId file_id, Status status); + void do_edit_story(FileId file_id, unique_ptr &&pending_story, + telegram_api::object_ptr input_file); + + void on_story_edited(FileId file_id, unique_ptr pending_story, Result result); + void on_toggle_story_is_pinned(StoryId story_id, bool is_pinned, Promise &&promise); std::shared_ptr upload_media_callback_; @@ -143,6 +160,8 @@ class StoryManager final : public Actor { WaitFreeHashMap, StoryFullIdHash> stories_; + FlatHashMap, StoryFullIdHash> being_edited_stories_; + uint32 send_story_count_ = 0; FlatHashMap, FileIdHash> being_uploaded_files_; diff --git a/td/telegram/Td.cpp b/td/telegram/Td.cpp index 1a3b22eee..04f76ecd8 100644 --- a/td/telegram/Td.cpp +++ b/td/telegram/Td.cpp @@ -5624,6 +5624,13 @@ void Td::on_request(uint64 id, td_api::sendStory &request) { std::move(request.privacy_rules_), request.is_pinned_, std::move(promise)); } +void Td::on_request(uint64 id, td_api::editStory &request) { + CHECK_IS_USER(); + CREATE_OK_REQUEST_PROMISE(); + story_manager_->edit_story(StoryId(request.story_id_), std::move(request.content_), std::move(request.caption_), + std::move(promise)); +} + void Td::on_request(uint64 id, td_api::setStoryPrivacyRules &request) { CHECK_IS_USER(); CREATE_OK_REQUEST_PROMISE(); diff --git a/td/telegram/Td.h b/td/telegram/Td.h index 6619d9625..a57697bf5 100644 --- a/td/telegram/Td.h +++ b/td/telegram/Td.h @@ -788,6 +788,8 @@ class Td final : public Actor { void on_request(uint64 id, td_api::sendStory &request); + void on_request(uint64 id, td_api::editStory &request); + void on_request(uint64 id, td_api::setStoryPrivacyRules &request); void on_request(uint64 id, const td_api::toggleStoryIsPinned &request); diff --git a/td/telegram/cli.cpp b/td/telegram/cli.cpp index 771bc5b1e..ee2ca84a7 100644 --- a/td/telegram/cli.cpp +++ b/td/telegram/cli.cpp @@ -3952,6 +3952,34 @@ class CliClient final : public Actor { td_api::make_object(as_input_file(video), to_integers(sticker_file_ids), duration), as_caption(caption), rules, op == "ssvp")); + } else if (op == "esc") { + StoryId story_id; + string caption; + get_args(args, story_id, caption); + send_request(td_api::make_object(story_id, nullptr, as_caption(caption))); + } else if (op == "esp") { + StoryId story_id; + string photo; + string caption; + string sticker_file_ids; + get_args(args, story_id, photo, caption, sticker_file_ids); + send_request( + td_api::make_object(story_id, + td_api::make_object( + as_input_file(photo), to_integers(sticker_file_ids)), + as_caption(caption))); + } else if (op == "esv") { + StoryId story_id; + string video; + string caption; + int32 duration; + string sticker_file_ids; + get_args(args, story_id, video, caption, duration, sticker_file_ids); + send_request(td_api::make_object( + story_id, + td_api::make_object(as_input_file(video), + to_integers(sticker_file_ids), duration), + as_caption(caption))); } else if (op == "sspr") { StoryId story_id; PrivacyRules rules;