diff --git a/td/generate/scheme/td_api.tl b/td/generate/scheme/td_api.tl index 3ca977758..f76f8b4ca 100644 --- a/td/generate/scheme/td_api.tl +++ b/td/generate/scheme/td_api.tl @@ -1332,7 +1332,7 @@ message id:int53 sender_id:MessageSender chat_id:int53 sending_state:MessageSend //@description Contains a list of messages @total_count Approximate total number of messages found @messages List of messages; messages may be null messages total_count:int32 messages:vector = Messages; -//@description Contains a list of messages found by a search @total_count Approximate total number of messages found; -1 if unknown @messages List of messages @next_offset The offset for the next request. If empty, there are no more results +//@description Contains a list of messages found by a search @total_count Approximate total number of messages found; -1 if unknown @messages List of messages @next_offset The offset for the next request. If empty, then there are no more results foundMessages total_count:int32 messages:vector next_offset:string = FoundMessages; //@description Contains a list of messages found by a search in a given chat @total_count Approximate total number of messages found; -1 if unknown @messages List of messages @next_from_message_id The offset for the next request. If 0, there are no more results @@ -1438,7 +1438,7 @@ downloadedFileCounts active_count:int32 paused_count:int32 completed_count:int32 //@description Contains a list of downloaded files, found by a search //@total_counts Total number of suitable files, ignoring offset //@files The list of files -//@next_offset The offset for the next request. If empty, there are no more results +//@next_offset The offset for the next request. If empty, then there are no more results foundFileDownloads total_counts:downloadedFileCounts files:vector next_offset:string = FoundFileDownloads; @@ -3386,7 +3386,7 @@ storyViewer user_id:int53 view_date:int32 block_list:BlockList chosen_reaction_t //@total_count Approximate total number of story viewers found //@total_reaction_count Approximate total number of reactions set by found story viewers //@viewers List of story viewers -//@next_offset The offset for the next request. If empty, there are no more results +//@next_offset The offset for the next request. If empty, then there are no more results storyViewers total_count:int32 total_reaction_count:int32 viewers:vector next_offset:string = StoryViewers; @@ -3567,6 +3567,22 @@ storyInfo story_id:int32 date:int32 is_for_close_friends:Bool = StoryInfo; chatActiveStories chat_id:int53 list:StoryList order:int53 max_read_story_id:int32 stories:vector = ChatActiveStories; +//@class StoryPublicForward @description Describes a public forward or repost of a story + +//@description Contains a public forward of a story as a message @message Information about the message with the story +storyPublicForwardMessage message:message = StoryPublicForward; + +//@description Contains a public repost of a story as a story @story Information about the reposted story +storyPublicForwardStory story:story = StoryPublicForward; + + +//@description Represents a list of public forwards and reposts of a story +//@total_count Approximate total number of messages and stories found +//@forwards List of found public forwards and reposts +//@next_offset The offset for the next request. If empty, then there are no more results +storyPublicForwards total_count:int32 forwards:vector next_offset:string = StoryPublicForwards; + + //@class ChatBoostSource @description Describes source of a chat boost //@description The chat created a Telegram Premium gift code for a user @@ -3614,7 +3630,7 @@ chatBoostStatus boost_url:string applied_slot_ids:vector level:int32 gift //@expiration_date Point in time (Unix timestamp) when the boost will expire chatBoost id:string count:int32 source:ChatBoostSource start_date:int32 expiration_date:int32 = ChatBoost; -//@description Contains a list of boosts applied to a chat @total_count Total number of boosts applied to the chat @boosts List of boosts @next_offset The offset for the next request. If empty, there are no more results +//@description Contains a list of boosts applied to a chat @total_count Total number of boosts applied to the chat @boosts List of boosts @next_offset The offset for the next request. If empty, then there are no more results foundChatBoosts total_count:int32 boosts:vector next_offset:string = FoundChatBoosts; //@description Describes a slot for chat boost @@ -3864,7 +3880,7 @@ phoneNumberAuthenticationSettings allow_flash_call:Bool allow_missed_call:Bool i //@date Point in time (Unix timestamp) when the reaction was added addedReaction type:ReactionType sender_id:MessageSender is_outgoing:Bool date:int32 = AddedReaction; -//@description Represents a list of reactions added to a message @total_count The total number of found reactions @reactions The list of added reactions @next_offset The offset for the next request. If empty, there are no more results +//@description Represents a list of reactions added to a message @total_count The total number of found reactions @reactions The list of added reactions @next_offset The offset for the next request. If empty, then there are no more results addedReactions total_count:int32 reactions:vector next_offset:string = AddedReactions; //@description Represents an available reaction @type Type of the reaction @needs_premium True, if Telegram Premium is needed to send the reaction @@ -4215,7 +4231,7 @@ inlineQueryResultsButton text:string type:InlineQueryResultsButtonType = InlineQ //@inline_query_id Unique identifier of the inline query //@button Button to be shown above inline query results; may be null //@results Results of the query -//@next_offset The offset for the next request. If empty, there are no more results +//@next_offset The offset for the next request. If empty, then there are no more results inlineQueryResults inline_query_id:int64 button:inlineQueryResultsButton results:vector next_offset:string = InlineQueryResults; @@ -7811,7 +7827,7 @@ setChatPermissions chat_id:int53 permissions:chatPermissions = Ok; setChatBackground chat_id:int53 background:InputBackground type:BackgroundType dark_theme_dimming:int32 only_for_self:Bool = Ok; //@description Restores background in a specific chat after it was changed by the other user. Supported only in private and secret chats with non-deleted users. -//-Can be called only from messageChatSetBackground messages with the currently set background that was set for both sides by the other user if userFullInfo.set_chat_background +//-Can be called only from messageChatSetBackground messages with the currently set background that was set for both sides by the other user if userFullInfo.set_chat_background == true //@chat_id Chat identifier revertChatBackground chat_id:int53 = Ok; @@ -8102,6 +8118,14 @@ reportStory story_sender_chat_id:int53 story_id:int32 reason:ReportReason text:s //-and for the next "story_stealth_mode_future_period" seconds; for Telegram Premium users only activateStoryStealthMode = Ok; +//@description Returns forwards of a story as a message to public chats and reposts by public channels. Can be used only if story.can_get_statistics == true, or the story is posted on behalf of the current user. +//-For optimal performance, the number of returned messages and stories is chosen by TDLib +//@story_sender_chat_id The identifier of the sender of the story +//@story_id The identifier of the story +//@offset Offset of the first entry to return as received from the previous request; use empty string to get the first chunk of results +//@limit The maximum number of messages and stories to be returned; must be positive and can't be greater than 100. For optimal performance, the number of returned objects is chosen by TDLib and can be smaller than the specified limit +getStoryPublicForwards story_sender_chat_id:int53 story_id:int32 offset:string limit:int32 = StoryPublicForwards; + //@description Returns the list of available chat boost slots for the current user getAvailableChatBoostSlots = ChatBoostSlots; diff --git a/td/telegram/MessagesManager.cpp b/td/telegram/MessagesManager.cpp index 32a4190dc..83b8649ca 100644 --- a/td/telegram/MessagesManager.cpp +++ b/td/telegram/MessagesManager.cpp @@ -9767,7 +9767,7 @@ void MessagesManager::get_channel_differences_if_needed(MessagesInfo &&messages_ auto dialog_id = DialogId::get_message_dialog_id(message); if (need_channel_difference_to_add_message(dialog_id, message)) { - run_after_channel_difference(dialog_id, MessageId::get_max_message_id(messages_info.messages), mpas.get_promise(), + run_after_channel_difference(dialog_id, MessageId::get_message_id(message, false), mpas.get_promise(), "get_channel_differences_if_needed"); } } @@ -9777,6 +9777,35 @@ void MessagesManager::get_channel_differences_if_needed(MessagesInfo &&messages_ lock.set_value(Unit()); } +void MessagesManager::get_channel_differences_if_needed( + telegram_api::object_ptr &&public_forwards, + Promise> &&promise) { + if (td_->auth_manager_->is_bot()) { + return promise.set_value(std::move(public_forwards)); + } + MultiPromiseActorSafe mpas{"GetChannelDifferencesIfNeededMultiPromiseActor"}; + mpas.add_promise(Promise()); + mpas.set_ignore_errors(true); + auto lock = mpas.get_promise(); + for (const auto &forward : public_forwards->forwards_) { + CHECK(forward != nullptr); + if (forward->get_id() != telegram_api::publicForwardMessage::ID) { + continue; + } + const auto &message = static_cast(forward.get())->message_; + auto dialog_id = DialogId::get_message_dialog_id(message); + if (need_channel_difference_to_add_message(dialog_id, message)) { + run_after_channel_difference(dialog_id, MessageId::get_message_id(message, false), mpas.get_promise(), + "get_channel_differences_if_needed"); + } + } + // must be added after forwarded messages are checked + mpas.add_promise( + PromiseCreator::lambda([public_forwards = std::move(public_forwards), promise = std::move(promise)]( + Unit ignored) mutable { promise.set_value(std::move(public_forwards)); })); + lock.set_value(Unit()); +} + void MessagesManager::on_get_messages(vector> &&messages, bool is_channel_message, bool is_scheduled, Promise &&promise, const char *source) { TRY_STATUS_PROMISE(promise, G()->close_status()); diff --git a/td/telegram/MessagesManager.h b/td/telegram/MessagesManager.h index 7f41f75e2..82a9779cf 100644 --- a/td/telegram/MessagesManager.h +++ b/td/telegram/MessagesManager.h @@ -178,6 +178,10 @@ class MessagesManager final : public Actor { void get_channel_differences_if_needed(MessagesInfo &&messages_info, Promise &&promise); + void get_channel_differences_if_needed( + telegram_api::object_ptr &&public_forwards, + Promise> &&promise); + void on_get_messages(vector> &&messages, bool is_channel_message, bool is_scheduled, Promise &&promise, const char *source); diff --git a/td/telegram/StatisticsManager.cpp b/td/telegram/StatisticsManager.cpp index d20c4fa64..726b8482d 100644 --- a/td/telegram/StatisticsManager.cpp +++ b/td/telegram/StatisticsManager.cpp @@ -443,6 +443,55 @@ class GetMessagePublicForwardsQuery final : public Td::ResultHandler { } }; +class GetStoryPublicForwardsQuery final : public Td::ResultHandler { + Promise> promise_; + DialogId dialog_id_; + + public: + explicit GetStoryPublicForwardsQuery(Promise> &&promise) + : promise_(std::move(promise)) { + } + + void send(DcId dc_id, StoryFullId story_full_id, const string &offset, int32 limit) { + dialog_id_ = story_full_id.get_dialog_id(); + + auto input_peer = td_->messages_manager_->get_input_peer(dialog_id_, AccessRights::Write); + if (input_peer == nullptr) { + return on_error(Status::Error(400, "Can't get story statistics")); + } + + send_query( + G()->net_query_creator().create(telegram_api::stats_getStoryPublicForwards( + std::move(input_peer), story_full_id.get_story_id().get(), offset, limit), + {}, dc_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()); + } + + td_->messages_manager_->get_channel_differences_if_needed( + result_ptr.move_as_ok(), + PromiseCreator::lambda( + [actor_id = td_->statistics_manager_actor_.get(), promise = std::move(promise_)]( + Result> &&result) mutable { + if (result.is_error()) { + promise.set_error(result.move_as_error()); + } else { + send_closure(actor_id, &StatisticsManager::on_get_story_public_forwards, result.move_as_ok(), + std::move(promise)); + } + })); + } + + void on_error(Status status) final { + td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetStoryPublicForwardsQuery"); + promise_.set_error(std::move(status)); + } +}; + StatisticsManager::StatisticsManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) { } @@ -662,4 +711,103 @@ void StatisticsManager::on_get_message_public_forwards( promise.set_value(td_api::make_object(total_count, std::move(result), next_offset)); } +void StatisticsManager::get_story_public_forwards(StoryFullId story_full_id, string offset, int32 limit, + Promise> &&promise) { + if (limit <= 0) { + return promise.set_error(Status::Error(400, "Parameter limit must be positive")); + } + auto dialog_id = story_full_id.get_dialog_id(); + if (dialog_id.get_type() == DialogType::User) { + if (dialog_id != DialogId(td_->contacts_manager_->get_my_id())) { + return promise.set_error(Status::Error(400, "Have no access to story statistics")); + } + return send_get_story_public_forwards_query(DcId::main(), story_full_id, std::move(offset), limit, + std::move(promise)); + } + + auto dc_id_promise = PromiseCreator::lambda([actor_id = actor_id(this), story_full_id, offset = std::move(offset), + limit, promise = std::move(promise)](Result r_dc_id) mutable { + if (r_dc_id.is_error()) { + return promise.set_error(r_dc_id.move_as_error()); + } + send_closure(actor_id, &StatisticsManager::send_get_story_public_forwards_query, r_dc_id.move_as_ok(), + story_full_id, std::move(offset), limit, std::move(promise)); + }); + td_->contacts_manager_->get_channel_statistics_dc_id(dialog_id, false, std::move(dc_id_promise)); +} + +void StatisticsManager::send_get_story_public_forwards_query( + DcId dc_id, StoryFullId story_full_id, string offset, int32 limit, + Promise> &&promise) { + if (!td_->story_manager_->have_story_force(story_full_id)) { + return promise.set_error(Status::Error(400, "Story not found")); + } + if (!td_->story_manager_->can_get_story_statistics(story_full_id) && + story_full_id.get_dialog_id() != DialogId(td_->contacts_manager_->get_my_id())) { + return promise.set_error(Status::Error(400, "Story forwards are inaccessible")); + } + + static constexpr int32 MAX_STORY_FORWARDS = 100; // server side limit + if (limit > MAX_STORY_FORWARDS) { + limit = MAX_STORY_FORWARDS; + } + + td_->create_handler(std::move(promise))->send(dc_id, story_full_id, offset, limit); +} + +void StatisticsManager::on_get_story_public_forwards( + telegram_api::object_ptr &&public_forwards, + Promise> &&promise) { + TRY_STATUS_PROMISE(promise, G()->close_status()); + + td_->contacts_manager_->on_get_users(std::move(public_forwards->users_), "on_get_story_public_forwards"); + td_->contacts_manager_->on_get_chats(std::move(public_forwards->chats_), "on_get_story_public_forwards"); + + auto total_count = public_forwards->count_; + LOG(INFO) << "Receive " << public_forwards->forwards_.size() << " forwarded stories out of " + << public_forwards->count_; + vector> result; + for (auto &forward_ptr : public_forwards->forwards_) { + switch (forward_ptr->get_id()) { + case telegram_api::publicForwardMessage::ID: { + auto forward = telegram_api::move_object_as(forward_ptr); + auto dialog_id = DialogId::get_message_dialog_id(forward->message_); + auto message_full_id = td_->messages_manager_->on_get_message(std::move(forward->message_), false, + dialog_id.get_type() == DialogType::Channel, + false, "on_get_story_public_forwards"); + if (message_full_id != MessageFullId()) { + CHECK(dialog_id == message_full_id.get_dialog_id()); + result.push_back(td_api::make_object( + td_->messages_manager_->get_message_object(message_full_id, "on_get_story_public_forwards"))); + CHECK(result.back() != nullptr); + } else { + total_count--; + } + break; + } + case telegram_api::publicForwardStory::ID: { + auto forward = telegram_api::move_object_as(forward_ptr); + auto dialog_id = DialogId(forward->peer_); + auto story_id = td_->story_manager_->on_get_story(dialog_id, std::move(forward->story_)); + if (story_id.is_valid() && td_->story_manager_->have_story({dialog_id, story_id})) { + result.push_back(td_api::make_object( + td_->story_manager_->get_story_object({dialog_id, story_id}))); + CHECK(result.back() != nullptr); + } else { + total_count--; + } + break; + } + default: + UNREACHABLE(); + } + } + if (total_count < static_cast(result.size())) { + LOG(ERROR) << "Receive " << result.size() << " valid story sorwards out of " << total_count; + total_count = static_cast(result.size()); + } + promise.set_value( + td_api::make_object(total_count, std::move(result), public_forwards->next_offset_)); +} + } // namespace td diff --git a/td/telegram/StatisticsManager.h b/td/telegram/StatisticsManager.h index af6ae5d12..f5f272f2b 100644 --- a/td/telegram/StatisticsManager.h +++ b/td/telegram/StatisticsManager.h @@ -46,6 +46,12 @@ class StatisticsManager final : public Actor { vector> &&messages, int32 next_rate, Promise> &&promise); + void get_story_public_forwards(StoryFullId story_full_id, string offset, int32 limit, + Promise> &&promise); + + void on_get_story_public_forwards(telegram_api::object_ptr &&public_forwards, + Promise> &&promise); + private: void tear_down() final; @@ -64,6 +70,9 @@ class StatisticsManager final : public Actor { void send_get_message_public_forwards_query(DcId dc_id, MessageFullId message_full_id, string offset, int32 limit, Promise> &&promise); + void send_get_story_public_forwards_query(DcId dc_id, StoryFullId story_full_id, string offset, int32 limit, + Promise> &&promise); + Td *td_; ActorShared<> parent_; }; diff --git a/td/telegram/Td.cpp b/td/telegram/Td.cpp index e626b07a4..4dbd0fc5f 100644 --- a/td/telegram/Td.cpp +++ b/td/telegram/Td.cpp @@ -5514,6 +5514,14 @@ void Td::on_request(uint64 id, td_api::getMessagePublicForwards &request) { std::move(request.offset_), request.limit_, std::move(promise)); } +void Td::on_request(uint64 id, td_api::getStoryPublicForwards &request) { + CHECK_IS_USER(); + CLEAN_INPUT_STRING(request.offset_); + CREATE_REQUEST_PROMISE(); + statistics_manager_->get_story_public_forwards({DialogId(request.story_sender_chat_id_), StoryId(request.story_id_)}, + std::move(request.offset_), request.limit_, std::move(promise)); +} + void Td::on_request(uint64 id, const td_api::removeNotification &request) { CHECK_IS_USER(); CREATE_OK_REQUEST_PROMISE(); diff --git a/td/telegram/Td.h b/td/telegram/Td.h index 0193c8658..9a58a9717 100644 --- a/td/telegram/Td.h +++ b/td/telegram/Td.h @@ -753,6 +753,8 @@ class Td final : public Actor { void on_request(uint64 id, td_api::getMessagePublicForwards &request); + void on_request(uint64 id, td_api::getStoryPublicForwards &request); + void on_request(uint64 id, const td_api::removeNotification &request); void on_request(uint64 id, const td_api::removeNotificationGroup &request); diff --git a/td/telegram/cli.cpp b/td/telegram/cli.cpp index 6af0fd82d..78526c355 100644 --- a/td/telegram/cli.cpp +++ b/td/telegram/cli.cpp @@ -2793,6 +2793,13 @@ class CliClient final : public Actor { string limit; get_args(args, chat_id, message_id, offset, limit); send_request(td_api::make_object(chat_id, message_id, offset, as_limit(limit))); + } else if (op == "gspf") { + ChatId chat_id; + StoryId story_id; + string offset; + string limit; + get_args(args, chat_id, story_id, offset, limit); + send_request(td_api::make_object(chat_id, story_id, offset, as_limit(limit))); } else if (op == "ghf") { get_history_chat_id_ = as_chat_id(args); send_request(td_api::make_object(get_history_chat_id_, std::numeric_limits::max(),