// // 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/StatisticsManager.h" #include "td/telegram/ContactsManager.h" #include "td/telegram/Global.h" #include "td/telegram/MessageId.h" #include "td/telegram/MessagesInfo.h" #include "td/telegram/MessagesManager.h" #include "td/telegram/ServerMessageId.h" #include "td/telegram/Td.h" #include "td/telegram/telegram_api.h" #include "td/telegram/UserId.h" #include "td/utils/algorithm.h" #include "td/utils/buffer.h" #include "td/utils/misc.h" #include "td/utils/Status.h" namespace td { static td_api::object_ptr convert_date_range( const telegram_api::object_ptr &obj) { return td_api::make_object(obj->min_date_, obj->max_date_); } static td_api::object_ptr convert_stats_graph( telegram_api::object_ptr obj) { CHECK(obj != nullptr); switch (obj->get_id()) { case telegram_api::statsGraphAsync::ID: { auto graph = move_tl_object_as(obj); return td_api::make_object(std::move(graph->token_)); } case telegram_api::statsGraphError::ID: { auto graph = move_tl_object_as(obj); return td_api::make_object(std::move(graph->error_)); } case telegram_api::statsGraph::ID: { auto graph = move_tl_object_as(obj); return td_api::make_object(std::move(graph->json_->data_), std::move(graph->zoom_token_)); } default: UNREACHABLE(); return nullptr; } } static double get_percentage_value(double part, double total) { if (total < 1e-6 && total > -1e-6) { if (part < 1e-6 && part > -1e-6) { return 0.0; } return 100.0; } if (part > 1e20) { return 100.0; } return clamp(0.0, part / total * 100, 100.0); } static td_api::object_ptr convert_stats_absolute_value( const telegram_api::object_ptr &obj) { return td_api::make_object( obj->current_, obj->previous_, get_percentage_value(obj->current_ - obj->previous_, obj->previous_)); } static td_api::object_ptr convert_megagroup_stats( Td *td, telegram_api::object_ptr obj) { CHECK(obj != nullptr); td->contacts_manager_->on_get_users(std::move(obj->users_), "convert_megagroup_stats"); // just in case td::remove_if(obj->top_posters_, [](auto &obj) { return !UserId(obj->user_id_).is_valid() || obj->messages_ < 0 || obj->avg_chars_ < 0; }); td::remove_if(obj->top_admins_, [](auto &obj) { return !UserId(obj->user_id_).is_valid() || obj->deleted_ < 0 || obj->kicked_ < 0 || obj->banned_ < 0; }); td::remove_if(obj->top_inviters_, [](auto &obj) { return !UserId(obj->user_id_).is_valid() || obj->invitations_ < 0; }); auto top_senders = transform( std::move(obj->top_posters_), [td](telegram_api::object_ptr &&top_poster) { return td_api::make_object( td->contacts_manager_->get_user_id_object(UserId(top_poster->user_id_), "get_top_senders"), top_poster->messages_, top_poster->avg_chars_); }); auto top_administrators = transform( std::move(obj->top_admins_), [td](telegram_api::object_ptr &&top_admin) { return td_api::make_object( td->contacts_manager_->get_user_id_object(UserId(top_admin->user_id_), "get_top_administrators"), top_admin->deleted_, top_admin->kicked_, top_admin->banned_); }); auto top_inviters = transform( std::move(obj->top_inviters_), [td](telegram_api::object_ptr &&top_inviter) { return td_api::make_object( td->contacts_manager_->get_user_id_object(UserId(top_inviter->user_id_), "get_top_inviters"), top_inviter->invitations_); }); return td_api::make_object( convert_date_range(obj->period_), convert_stats_absolute_value(obj->members_), convert_stats_absolute_value(obj->messages_), convert_stats_absolute_value(obj->viewers_), convert_stats_absolute_value(obj->posters_), convert_stats_graph(std::move(obj->growth_graph_)), convert_stats_graph(std::move(obj->members_graph_)), convert_stats_graph(std::move(obj->new_members_by_source_graph_)), convert_stats_graph(std::move(obj->languages_graph_)), convert_stats_graph(std::move(obj->messages_graph_)), convert_stats_graph(std::move(obj->actions_graph_)), convert_stats_graph(std::move(obj->top_hours_graph_)), convert_stats_graph(std::move(obj->weekdays_graph_)), std::move(top_senders), std::move(top_administrators), std::move(top_inviters)); } static td_api::object_ptr convert_broadcast_stats( telegram_api::object_ptr obj) { CHECK(obj != nullptr); auto recent_message_interactions = transform(std::move(obj->recent_message_interactions_), [](auto &&interaction) { return td_api::make_object( MessageId(ServerMessageId(interaction->msg_id_)).get(), interaction->views_, interaction->forwards_); }); return td_api::make_object( convert_date_range(obj->period_), convert_stats_absolute_value(obj->followers_), convert_stats_absolute_value(obj->views_per_post_), convert_stats_absolute_value(obj->shares_per_post_), get_percentage_value(obj->enabled_notifications_->part_, obj->enabled_notifications_->total_), convert_stats_graph(std::move(obj->growth_graph_)), convert_stats_graph(std::move(obj->followers_graph_)), convert_stats_graph(std::move(obj->mute_graph_)), convert_stats_graph(std::move(obj->top_hours_graph_)), convert_stats_graph(std::move(obj->views_by_source_graph_)), convert_stats_graph(std::move(obj->new_followers_by_source_graph_)), convert_stats_graph(std::move(obj->languages_graph_)), convert_stats_graph(std::move(obj->interactions_graph_)), convert_stats_graph(std::move(obj->iv_interactions_graph_)), std::move(recent_message_interactions)); } class GetMegagroupStatsQuery final : public Td::ResultHandler { Promise> promise_; ChannelId channel_id_; public: explicit GetMegagroupStatsQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(ChannelId channel_id, bool is_dark, DcId dc_id) { channel_id_ = channel_id; auto input_channel = td_->contacts_manager_->get_input_channel(channel_id); CHECK(input_channel != nullptr); int32 flags = 0; if (is_dark) { flags |= telegram_api::stats_getMegagroupStats::DARK_MASK; } send_query(G()->net_query_creator().create( telegram_api::stats_getMegagroupStats(flags, false /*ignored*/, std::move(input_channel)), {}, 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()); } promise_.set_value(convert_megagroup_stats(td_, result_ptr.move_as_ok())); } void on_error(Status status) final { td_->contacts_manager_->on_get_channel_error(channel_id_, status, "GetMegagroupStatsQuery"); promise_.set_error(std::move(status)); } }; class GetBroadcastStatsQuery final : public Td::ResultHandler { Promise> promise_; ChannelId channel_id_; public: explicit GetBroadcastStatsQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(ChannelId channel_id, bool is_dark, DcId dc_id) { channel_id_ = channel_id; auto input_channel = td_->contacts_manager_->get_input_channel(channel_id); CHECK(input_channel != nullptr); int32 flags = 0; if (is_dark) { flags |= telegram_api::stats_getBroadcastStats::DARK_MASK; } send_query(G()->net_query_creator().create( telegram_api::stats_getBroadcastStats(flags, false /*ignored*/, std::move(input_channel)), {}, 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()); } auto result = convert_broadcast_stats(result_ptr.move_as_ok()); for (auto &info : result->recent_message_interactions_) { td_->messages_manager_->on_update_message_interaction_info({DialogId(channel_id_), MessageId(info->message_id_)}, info->view_count_, info->forward_count_, false, nullptr); } promise_.set_value(std::move(result)); } void on_error(Status status) final { td_->contacts_manager_->on_get_channel_error(channel_id_, status, "GetBroadcastStatsQuery"); promise_.set_error(std::move(status)); } }; static td_api::object_ptr convert_message_stats( telegram_api::object_ptr obj) { return td_api::make_object(convert_stats_graph(std::move(obj->views_graph_))); } class GetMessageStatsQuery final : public Td::ResultHandler { Promise> promise_; ChannelId channel_id_; public: explicit GetMessageStatsQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(ChannelId channel_id, MessageId message_id, bool is_dark, DcId dc_id) { channel_id_ = channel_id; auto input_channel = td_->contacts_manager_->get_input_channel(channel_id); if (input_channel == nullptr) { return promise_.set_error(Status::Error(400, "Supergroup not found")); } int32 flags = 0; if (is_dark) { flags |= telegram_api::stats_getMessageStats::DARK_MASK; } send_query(G()->net_query_creator().create( telegram_api::stats_getMessageStats(flags, false /*ignored*/, std::move(input_channel), message_id.get_server_message_id().get()), {}, 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()); } promise_.set_value(convert_message_stats(result_ptr.move_as_ok())); } void on_error(Status status) final { td_->contacts_manager_->on_get_channel_error(channel_id_, status, "GetMessageStatsQuery"); promise_.set_error(std::move(status)); } }; class LoadAsyncGraphQuery final : public Td::ResultHandler { Promise> promise_; public: explicit LoadAsyncGraphQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(const string &token, int64 x, DcId dc_id) { int32 flags = 0; if (x != 0) { flags |= telegram_api::stats_loadAsyncGraph::X_MASK; } send_query(G()->net_query_creator().create(telegram_api::stats_loadAsyncGraph(flags, token, x), {}, 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()); } auto result = result_ptr.move_as_ok(); promise_.set_value(convert_stats_graph(std::move(result))); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; class GetMessagePublicForwardsQuery final : public Td::ResultHandler { Promise> promise_; DialogId dialog_id_; int32 limit_; public: explicit GetMessagePublicForwardsQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(DcId dc_id, MessageFullId message_full_id, int32 offset_date, DialogId offset_dialog_id, ServerMessageId offset_message_id, int32 limit) { dialog_id_ = message_full_id.get_dialog_id(); limit_ = limit; auto input_peer = MessagesManager::get_input_peer_force(offset_dialog_id); CHECK(input_peer != nullptr); auto input_channel = td_->contacts_manager_->get_input_channel(dialog_id_.get_channel_id()); CHECK(input_channel != nullptr); send_query(G()->net_query_creator().create( telegram_api::stats_getMessagePublicForwards( std::move(input_channel), message_full_id.get_message_id().get_server_message_id().get(), offset_date, std::move(input_peer), offset_message_id.get(), 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()); } auto info = get_messages_info(td_, DialogId(), result_ptr.move_as_ok(), "GetMessagePublicForwardsQuery"); td_->messages_manager_->get_channel_differences_if_needed( std::move(info), 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 { auto info = result.move_as_ok(); send_closure(actor_id, &StatisticsManager::on_get_message_public_forwards, info.total_count, std::move(info.messages), info.next_rate, std::move(promise)); } })); } void on_error(Status status) final { td_->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetMessagePublicForwardsQuery"); promise_.set_error(std::move(status)); } }; StatisticsManager::StatisticsManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) { } void StatisticsManager::tear_down() { parent_.reset(); } void StatisticsManager::get_channel_statistics(DialogId dialog_id, bool is_dark, Promise> &&promise) { auto dc_id_promise = PromiseCreator::lambda( [actor_id = actor_id(this), dialog_id, is_dark, 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_channel_stats_query, r_dc_id.move_as_ok(), dialog_id.get_channel_id(), is_dark, std::move(promise)); }); td_->contacts_manager_->get_channel_statistics_dc_id(dialog_id, true, std::move(dc_id_promise)); } void StatisticsManager::send_get_channel_stats_query(DcId dc_id, ChannelId channel_id, bool is_dark, Promise> &&promise) { TRY_STATUS_PROMISE(promise, G()->close_status()); if (td_->contacts_manager_->is_megagroup_channel(channel_id)) { td_->create_handler(std::move(promise))->send(channel_id, is_dark, dc_id); } else { td_->create_handler(std::move(promise))->send(channel_id, is_dark, dc_id); } } void StatisticsManager::get_channel_message_statistics( MessageFullId message_full_id, bool is_dark, Promise> &&promise) { auto dc_id_promise = PromiseCreator::lambda([actor_id = actor_id(this), message_full_id, is_dark, 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_channel_message_stats_query, r_dc_id.move_as_ok(), message_full_id, is_dark, std::move(promise)); }); td_->contacts_manager_->get_channel_statistics_dc_id(message_full_id.get_dialog_id(), false, std::move(dc_id_promise)); } void StatisticsManager::send_get_channel_message_stats_query( DcId dc_id, MessageFullId message_full_id, bool is_dark, Promise> &&promise) { TRY_STATUS_PROMISE(promise, G()->close_status()); auto dialog_id = message_full_id.get_dialog_id(); if (!td_->messages_manager_->have_message_force(message_full_id, "send_get_channel_message_stats_query")) { return promise.set_error(Status::Error(400, "Message not found")); } if (!td_->messages_manager_->can_get_message_statistics(message_full_id)) { return promise.set_error(Status::Error(400, "Message statistics is inaccessible")); } CHECK(dialog_id.get_type() == DialogType::Channel); td_->create_handler(std::move(promise)) ->send(dialog_id.get_channel_id(), message_full_id.get_message_id(), is_dark, dc_id); } void StatisticsManager::load_statistics_graph(DialogId dialog_id, string token, int64 x, Promise> &&promise) { auto dc_id_promise = PromiseCreator::lambda([actor_id = actor_id(this), token = std::move(token), x, 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_load_async_graph_query, r_dc_id.move_as_ok(), std::move(token), x, std::move(promise)); }); td_->contacts_manager_->get_channel_statistics_dc_id(dialog_id, false, std::move(dc_id_promise)); } void StatisticsManager::send_load_async_graph_query(DcId dc_id, string token, int64 x, Promise> &&promise) { TRY_STATUS_PROMISE(promise, G()->close_status()); td_->create_handler(std::move(promise))->send(token, x, dc_id); } void StatisticsManager::get_message_public_forwards(MessageFullId message_full_id, string offset, int32 limit, Promise> &&promise) { if (limit <= 0) { return promise.set_error(Status::Error(400, "Parameter limit must be positive")); } auto dc_id_promise = PromiseCreator::lambda([actor_id = actor_id(this), message_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_message_public_forwards_query, r_dc_id.move_as_ok(), message_full_id, std::move(offset), limit, std::move(promise)); }); td_->contacts_manager_->get_channel_statistics_dc_id(message_full_id.get_dialog_id(), false, std::move(dc_id_promise)); } void StatisticsManager::send_get_message_public_forwards_query( DcId dc_id, MessageFullId message_full_id, string offset, int32 limit, Promise> &&promise) { if (!td_->messages_manager_->have_message_force(message_full_id, "send_get_message_public_forwards_query")) { return promise.set_error(Status::Error(400, "Message not found")); } if (!td_->messages_manager_->can_get_message_statistics(message_full_id)) { return promise.set_error(Status::Error(400, "Message forwards are inaccessible")); } static constexpr int32 MAX_MESSAGE_FORWARDS = 100; // server side limit if (limit > MAX_MESSAGE_FORWARDS) { limit = MAX_MESSAGE_FORWARDS; } int32 offset_date = std::numeric_limits::max(); DialogId offset_dialog_id; ServerMessageId offset_server_message_id; if (!offset.empty()) { auto parts = full_split(offset, ','); if (parts.size() != 3) { return promise.set_error(Status::Error(400, "Invalid offset specified")); } auto r_offset_date = to_integer_safe(parts[0]); auto r_offset_dialog_id = to_integer_safe(parts[1]); auto r_offset_server_message_id = to_integer_safe(parts[2]); if (r_offset_date.is_error() || r_offset_dialog_id.is_error() || r_offset_server_message_id.is_error()) { return promise.set_error(Status::Error(400, "Invalid offset specified")); } offset_date = r_offset_date.ok(); offset_dialog_id = DialogId(r_offset_dialog_id.ok()); offset_server_message_id = ServerMessageId(r_offset_server_message_id.ok()); } td_->create_handler(std::move(promise)) ->send(dc_id, message_full_id, offset_date, offset_dialog_id, offset_server_message_id, limit); } void StatisticsManager::on_get_message_public_forwards( int32 total_count, vector> &&messages, int32 next_rate, Promise> &&promise) { TRY_STATUS_PROMISE(promise, G()->close_status()); LOG(INFO) << "Receive " << messages.size() << " forwarded messages"; vector> result; int32 last_message_date = 0; MessageId last_message_id; DialogId last_dialog_id; for (auto &message : messages) { auto message_date = MessagesManager::get_message_date(message); auto message_id = MessageId::get_message_id(message, false); auto dialog_id = DialogId::get_message_dialog_id(message); if (message_date > 0 && message_id.is_valid() && dialog_id.is_valid()) { last_message_date = message_date; last_message_id = message_id; last_dialog_id = dialog_id; } auto new_message_full_id = td_->messages_manager_->on_get_message(std::move(message), false, dialog_id.get_type() == DialogType::Channel, false, "on_get_message_public_forwards"); if (new_message_full_id != MessageFullId()) { CHECK(dialog_id == new_message_full_id.get_dialog_id()); result.push_back( td_->messages_manager_->get_message_object(new_message_full_id, "on_get_message_public_forwards")); CHECK(result.back() != nullptr); } else { total_count--; } } if (total_count < static_cast(result.size())) { LOG(ERROR) << "Receive " << result.size() << " valid messages out of " << total_count << " in " << messages.size() << " messages"; total_count = static_cast(result.size()); } string next_offset; if (!result.empty()) { if (next_rate > 0) { last_message_date = next_rate; } next_offset = PSTRING() << last_message_date << ',' << last_dialog_id.get() << ',' << last_message_id.get_server_message_id().get(); } promise.set_value(td_api::make_object(total_count, std::move(result), next_offset)); } } // namespace td