// // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2024 // // 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/ChannelRecommendationManager.h" #include "td/telegram/AccessRights.h" #include "td/telegram/Application.h" #include "td/telegram/ChatManager.h" #include "td/telegram/Dependencies.h" #include "td/telegram/DialogManager.h" #include "td/telegram/Global.h" #include "td/telegram/logevent/LogEvent.h" #include "td/telegram/logevent/LogEventHelper.h" #include "td/telegram/OptionManager.h" #include "td/telegram/Td.h" #include "td/telegram/TdDb.h" #include "td/db/SqliteKeyValueAsync.h" #include "td/utils/buffer.h" #include "td/utils/logging.h" #include "td/utils/SliceBuilder.h" #include "td/utils/Time.h" #include "td/utils/tl_helpers.h" namespace td { class GetChannelRecommendationsQuery final : public Td::ResultHandler { Promise>>> promise_; ChannelId channel_id_; public: explicit GetChannelRecommendationsQuery( Promise>>> &&promise) : promise_(std::move(promise)) { } void send(ChannelId channel_id) { channel_id_ = channel_id; auto input_channel = td_->chat_manager_->get_input_channel(channel_id); CHECK(!channel_id.is_valid() || input_channel != nullptr); int32 flags = 0; if (input_channel != nullptr) { flags |= telegram_api::channels_getChannelRecommendations::CHANNEL_MASK; } send_query(G()->net_query_creator().create( telegram_api::channels_getChannelRecommendations(flags, std::move(input_channel)))); } 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 chats_ptr = result_ptr.move_as_ok(); LOG(INFO) << "Receive result for GetChannelRecommendationsQuery: " << to_string(chats_ptr); switch (chats_ptr->get_id()) { case telegram_api::messages_chats::ID: { auto chats = move_tl_object_as(chats_ptr); auto total_count = static_cast(chats->chats_.size()); return promise_.set_value({total_count, std::move(chats->chats_)}); } case telegram_api::messages_chatsSlice::ID: { auto chats = move_tl_object_as(chats_ptr); return promise_.set_value({chats->count_, std::move(chats->chats_)}); } default: UNREACHABLE(); return promise_.set_error(Status::Error("Unreachable")); } } void on_error(Status status) final { if (channel_id_.is_valid()) { td_->chat_manager_->on_get_channel_error(channel_id_, status, "GetChannelRecommendationsQuery"); } promise_.set_error(std::move(status)); } }; template void ChannelRecommendationManager::RecommendedDialogs::store(StorerT &storer) const { bool has_dialog_ids = !dialog_ids_.empty(); bool has_total_count = static_cast(total_count_) != dialog_ids_.size(); BEGIN_STORE_FLAGS(); STORE_FLAG(has_dialog_ids); STORE_FLAG(has_total_count); END_STORE_FLAGS(); if (has_dialog_ids) { td::store(dialog_ids_, storer); } store_time(next_reload_time_, storer); if (has_total_count) { td::store(total_count_, storer); } } template void ChannelRecommendationManager::RecommendedDialogs::parse(ParserT &parser) { bool has_dialog_ids; bool has_total_count; BEGIN_PARSE_FLAGS(); PARSE_FLAG(has_dialog_ids); PARSE_FLAG(has_total_count); END_PARSE_FLAGS(); if (has_dialog_ids) { td::parse(dialog_ids_, parser); } parse_time(next_reload_time_, parser); if (has_total_count) { td::parse(total_count_, parser); } else { total_count_ = static_cast(dialog_ids_.size()); } } ChannelRecommendationManager::ChannelRecommendationManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) { if (G()->use_sqlite_pmc() && !G()->use_message_database()) { G()->td_db()->get_sqlite_pmc()->erase_by_prefix("channel_recommendations", Auto()); } } void ChannelRecommendationManager::tear_down() { parent_.reset(); } bool ChannelRecommendationManager::is_suitable_recommended_channel(DialogId dialog_id) const { if (dialog_id.get_type() != DialogType::Channel) { return false; } return is_suitable_recommended_channel(dialog_id.get_channel_id()); } bool ChannelRecommendationManager::is_suitable_recommended_channel(ChannelId channel_id) const { auto status = td_->chat_manager_->get_channel_status(channel_id); return !status.is_member() && td_->chat_manager_->have_input_peer_channel(channel_id, AccessRights::Read); } bool ChannelRecommendationManager::are_suitable_recommended_dialogs( const RecommendedDialogs &recommended_dialogs) const { for (auto recommended_dialog_id : recommended_dialogs.dialog_ids_) { if (!is_suitable_recommended_channel(recommended_dialog_id)) { return false; } } auto is_premium = td_->option_manager_->get_option_boolean("is_premium"); auto have_all = recommended_dialogs.dialog_ids_.size() == static_cast(recommended_dialogs.total_count_); if (!have_all && is_premium) { return false; } return true; } void ChannelRecommendationManager::get_recommended_channels(Promise> &&promise) { bool use_database = true; if (are_recommended_channels_inited_) { if (are_suitable_recommended_dialogs(recommended_channels_)) { auto next_reload_time = recommended_channels_.next_reload_time_; promise.set_value(td_->dialog_manager_->get_chats_object( recommended_channels_.total_count_, recommended_channels_.dialog_ids_, "get_recommended_channels")); if (next_reload_time > Time::now()) { return; } promise = {}; } else { LOG(INFO) << "Drop cache for recommended chats"; are_recommended_channels_inited_ = false; if (G()->use_message_database()) { G()->td_db()->get_sqlite_pmc()->erase(get_recommended_channels_database_key(), Auto()); } } use_database = false; } load_recommended_channels(use_database, std::move(promise)); } string ChannelRecommendationManager::get_recommended_channels_database_key() { return "recommended_channels"; } void ChannelRecommendationManager::load_recommended_channels(bool use_database, Promise> &&promise) { get_recommended_channels_queries_.push_back(std::move(promise)); if (get_recommended_channels_queries_.size() == 1) { if (G()->use_message_database() && use_database) { G()->td_db()->get_sqlite_pmc()->get( get_recommended_channels_database_key(), PromiseCreator::lambda([actor_id = actor_id(this)](string value) { send_closure(actor_id, &ChannelRecommendationManager::on_load_recommended_channels_from_database, std::move(value)); })); } else { reload_recommended_channels(); } } } void ChannelRecommendationManager::fail_load_recommended_channels_queries(Status &&error) { CHECK(!get_recommended_channels_queries_.empty()); fail_promises(get_recommended_channels_queries_, std::move(error)); } void ChannelRecommendationManager::finish_load_recommended_channels_queries(int32 total_count, vector dialog_ids) { are_recommended_channels_inited_ = true; auto promises = std::move(get_recommended_channels_queries_); CHECK(!promises.empty()); for (auto &promise : promises) { if (promise) { promise.set_value( td_->dialog_manager_->get_chats_object(total_count, dialog_ids, "finish_load_recommended_channels_queries")); } } } void ChannelRecommendationManager::on_load_recommended_channels_from_database(string value) { if (G()->close_flag()) { return fail_load_recommended_channels_queries(G()->close_status()); } if (value.empty()) { return reload_recommended_channels(); } if (log_event_parse(recommended_channels_, value).is_error()) { recommended_channels_ = {}; G()->td_db()->get_sqlite_pmc()->erase(get_recommended_channels_database_key(), Auto()); return reload_recommended_channels(); } Dependencies dependencies; for (auto dialog_id : recommended_channels_.dialog_ids_) { dependencies.add_dialog_and_dependencies(dialog_id); } if (!dependencies.resolve_force(td_, "on_load_recommended_channels_from_database") || !are_suitable_recommended_dialogs(recommended_channels_)) { recommended_channels_ = {}; G()->td_db()->get_sqlite_pmc()->erase(get_recommended_channels_database_key(), Auto()); return reload_recommended_channels(); } auto next_reload_time = recommended_channels_.next_reload_time_; finish_load_recommended_channels_queries(recommended_channels_.total_count_, recommended_channels_.dialog_ids_); if (next_reload_time <= Time::now()) { load_recommended_channels(false, Auto()); } } void ChannelRecommendationManager::reload_recommended_channels() { auto query_promise = PromiseCreator::lambda( [actor_id = actor_id(this)](Result>>> &&result) { send_closure(actor_id, &ChannelRecommendationManager::on_get_recommended_channels, std::move(result)); }); td_->create_handler(std::move(query_promise))->send(ChannelId()); } void ChannelRecommendationManager::on_get_recommended_channels( Result>>> &&r_chats) { G()->ignore_result_if_closing(r_chats); if (r_chats.is_error()) { return fail_load_recommended_channels_queries(r_chats.move_as_error()); } auto chats = r_chats.move_as_ok(); auto total_count = chats.first; auto channel_ids = td_->chat_manager_->get_channel_ids(std::move(chats.second), "on_get_recommended_channels"); vector dialog_ids; if (total_count < static_cast(channel_ids.size())) { LOG(ERROR) << "Receive total_count = " << total_count << " and " << channel_ids.size() << " recommended chats"; total_count = static_cast(channel_ids.size()); } for (auto recommended_channel_id : channel_ids) { auto recommended_dialog_id = DialogId(recommended_channel_id); td_->dialog_manager_->force_create_dialog(recommended_dialog_id, "on_get_recommended_channels"); if (is_suitable_recommended_channel(recommended_channel_id)) { dialog_ids.push_back(recommended_dialog_id); } else { total_count--; } } recommended_channels_.total_count_ = total_count; recommended_channels_.dialog_ids_ = dialog_ids; recommended_channels_.next_reload_time_ = Time::now() + CHANNEL_RECOMMENDATIONS_CACHE_TIME; if (G()->use_message_database()) { G()->td_db()->get_sqlite_pmc()->set(get_recommended_channels_database_key(), log_event_store(recommended_channels_).as_slice().str(), Promise()); } finish_load_recommended_channels_queries(total_count, std::move(dialog_ids)); } void ChannelRecommendationManager::get_channel_recommendations( DialogId dialog_id, bool return_local, Promise> &&chats_promise, Promise> &&count_promise) { if (!td_->dialog_manager_->have_dialog_force(dialog_id, "get_channel_recommendations")) { if (chats_promise) { chats_promise.set_error(Status::Error(400, "Chat not found")); } if (count_promise) { count_promise.set_error(Status::Error(400, "Chat not found")); } return; } if (dialog_id.get_type() != DialogType::Channel) { if (chats_promise) { chats_promise.set_value(td_api::make_object()); } if (count_promise) { count_promise.set_value(td_api::make_object(0)); } return; } auto channel_id = dialog_id.get_channel_id(); if (!td_->chat_manager_->is_broadcast_channel(channel_id) || td_->chat_manager_->get_input_channel(channel_id) == nullptr) { if (chats_promise) { chats_promise.set_value(td_api::make_object()); } if (count_promise) { count_promise.set_value(td_api::make_object(0)); } return; } bool use_database = true; auto it = channel_recommended_dialogs_.find(channel_id); if (it != channel_recommended_dialogs_.end()) { if (are_suitable_recommended_dialogs(it->second)) { auto next_reload_time = it->second.next_reload_time_; if (chats_promise) { chats_promise.set_value(td_->dialog_manager_->get_chats_object(it->second.total_count_, it->second.dialog_ids_, "get_channel_recommendations")); } if (count_promise) { count_promise.set_value(td_api::make_object(it->second.total_count_)); } if (next_reload_time > Time::now()) { return; } chats_promise = {}; count_promise = {}; } else { LOG(INFO) << "Drop cache for similar chats of " << dialog_id; channel_recommended_dialogs_.erase(it); if (G()->use_message_database()) { G()->td_db()->get_sqlite_pmc()->erase(get_channel_recommendations_database_key(channel_id), Auto()); } } use_database = false; } load_channel_recommendations(channel_id, use_database, return_local, std::move(chats_promise), std::move(count_promise)); } string ChannelRecommendationManager::get_channel_recommendations_database_key(ChannelId channel_id) { return PSTRING() << "channel_recommendations" << channel_id.get(); } void ChannelRecommendationManager::load_channel_recommendations( ChannelId channel_id, bool use_database, bool return_local, Promise> &&chats_promise, Promise> &&count_promise) { if (count_promise) { get_channel_recommendation_count_queries_[return_local][channel_id].push_back(std::move(count_promise)); } auto &queries = get_channel_recommendations_queries_[channel_id]; queries.push_back(std::move(chats_promise)); if (queries.size() == 1) { if (G()->use_message_database() && use_database) { G()->td_db()->get_sqlite_pmc()->get( get_channel_recommendations_database_key(channel_id), PromiseCreator::lambda([actor_id = actor_id(this), channel_id](string value) { send_closure(actor_id, &ChannelRecommendationManager::on_load_channel_recommendations_from_database, channel_id, std::move(value)); })); } else { reload_channel_recommendations(channel_id); } } } void ChannelRecommendationManager::fail_load_channel_recommendations_queries(ChannelId channel_id, Status &&error) { for (int return_local = 0; return_local < 2; return_local++) { auto it = get_channel_recommendation_count_queries_[return_local].find(channel_id); if (it != get_channel_recommendation_count_queries_[return_local].end()) { auto promises = std::move(it->second); CHECK(!promises.empty()); get_channel_recommendation_count_queries_[return_local].erase(it); fail_promises(promises, error.clone()); } } auto it = get_channel_recommendations_queries_.find(channel_id); CHECK(it != get_channel_recommendations_queries_.end()); auto promises = std::move(it->second); CHECK(!promises.empty()); get_channel_recommendations_queries_.erase(it); fail_promises(promises, std::move(error)); } void ChannelRecommendationManager::finish_load_channel_recommendations_queries(ChannelId channel_id, int32 total_count, vector dialog_ids) { for (int return_local = 0; return_local < 2; return_local++) { auto it = get_channel_recommendation_count_queries_[return_local].find(channel_id); if (it != get_channel_recommendation_count_queries_[return_local].end()) { auto promises = std::move(it->second); CHECK(!promises.empty()); get_channel_recommendation_count_queries_[return_local].erase(it); for (auto &promise : promises) { promise.set_value(td_api::make_object(total_count)); } } } auto it = get_channel_recommendations_queries_.find(channel_id); CHECK(it != get_channel_recommendations_queries_.end()); auto promises = std::move(it->second); CHECK(!promises.empty()); get_channel_recommendations_queries_.erase(it); for (auto &promise : promises) { if (promise) { promise.set_value(td_->dialog_manager_->get_chats_object(total_count, dialog_ids, "finish_load_channel_recommendations_queries")); } } } void ChannelRecommendationManager::on_load_channel_recommendations_from_database(ChannelId channel_id, string value) { if (G()->close_flag()) { return fail_load_channel_recommendations_queries(channel_id, G()->close_status()); } if (value.empty()) { return reload_channel_recommendations(channel_id); } auto &recommended_dialogs = channel_recommended_dialogs_[channel_id]; if (log_event_parse(recommended_dialogs, value).is_error()) { channel_recommended_dialogs_.erase(channel_id); G()->td_db()->get_sqlite_pmc()->erase(get_channel_recommendations_database_key(channel_id), Auto()); return reload_channel_recommendations(channel_id); } Dependencies dependencies; for (auto dialog_id : recommended_dialogs.dialog_ids_) { dependencies.add_dialog_and_dependencies(dialog_id); } if (!dependencies.resolve_force(td_, "on_load_channel_recommendations_from_database") || !are_suitable_recommended_dialogs(recommended_dialogs)) { channel_recommended_dialogs_.erase(channel_id); G()->td_db()->get_sqlite_pmc()->erase(get_channel_recommendations_database_key(channel_id), Auto()); return reload_channel_recommendations(channel_id); } auto next_reload_time = recommended_dialogs.next_reload_time_; finish_load_channel_recommendations_queries(channel_id, recommended_dialogs.total_count_, recommended_dialogs.dialog_ids_); if (next_reload_time <= Time::now()) { load_channel_recommendations(channel_id, false, false, Auto(), Auto()); } } void ChannelRecommendationManager::reload_channel_recommendations(ChannelId channel_id) { auto it = get_channel_recommendation_count_queries_[1].find(channel_id); if (it != get_channel_recommendation_count_queries_[1].end()) { auto promises = std::move(it->second); CHECK(!promises.empty()); get_channel_recommendation_count_queries_[1].erase(it); for (auto &promise : promises) { promise.set_value(td_api::make_object(-1)); } } auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), channel_id]( Result>>> &&result) { send_closure(actor_id, &ChannelRecommendationManager::on_get_channel_recommendations, channel_id, std::move(result)); }); td_->create_handler(std::move(query_promise))->send(channel_id); } void ChannelRecommendationManager::on_get_channel_recommendations( ChannelId channel_id, Result>>> &&r_chats) { G()->ignore_result_if_closing(r_chats); if (r_chats.is_error()) { return fail_load_channel_recommendations_queries(channel_id, r_chats.move_as_error()); } auto chats = r_chats.move_as_ok(); auto total_count = chats.first; auto channel_ids = td_->chat_manager_->get_channel_ids(std::move(chats.second), "on_get_channel_recommendations"); vector dialog_ids; if (total_count < static_cast(channel_ids.size())) { LOG(ERROR) << "Receive total_count = " << total_count << " and " << channel_ids.size() << " similar chats for " << channel_id; total_count = static_cast(channel_ids.size()); } for (auto recommended_channel_id : channel_ids) { auto recommended_dialog_id = DialogId(recommended_channel_id); td_->dialog_manager_->force_create_dialog(recommended_dialog_id, "on_get_channel_recommendations"); if (is_suitable_recommended_channel(recommended_channel_id)) { dialog_ids.push_back(recommended_dialog_id); } else { total_count--; } } auto &recommended_dialogs = channel_recommended_dialogs_[channel_id]; recommended_dialogs.total_count_ = total_count; recommended_dialogs.dialog_ids_ = dialog_ids; recommended_dialogs.next_reload_time_ = Time::now() + CHANNEL_RECOMMENDATIONS_CACHE_TIME; if (G()->use_message_database()) { G()->td_db()->get_sqlite_pmc()->set(get_channel_recommendations_database_key(channel_id), log_event_store(recommended_dialogs).as_slice().str(), Promise()); } finish_load_channel_recommendations_queries(channel_id, total_count, std::move(dialog_ids)); } void ChannelRecommendationManager::open_channel_recommended_channel(DialogId dialog_id, DialogId opened_dialog_id, Promise &&promise) { if (!td_->dialog_manager_->have_dialog_force(dialog_id, "open_channel_recommended_channel") || !td_->dialog_manager_->have_dialog_force(opened_dialog_id, "open_channel_recommended_channel")) { return promise.set_error(Status::Error(400, "Chat not found")); } if (dialog_id.get_type() != DialogType::Channel || opened_dialog_id.get_type() != DialogType::Channel) { return promise.set_error(Status::Error(400, "Invalid chat specified")); } vector> data; data.push_back(telegram_api::make_object( "ref_channel_id", make_tl_object(to_string(dialog_id.get_channel_id().get())))); data.push_back(telegram_api::make_object( "open_channel_id", make_tl_object(to_string(opened_dialog_id.get_channel_id().get())))); save_app_log(td_, "channels.open_recommended_channel", DialogId(), telegram_api::make_object(std::move(data)), std::move(promise)); } } // namespace td