// // 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/ThemeManager.h" #include "td/telegram/AuthManager.h" #include "td/telegram/BackgroundInfo.hpp" #include "td/telegram/Global.h" #include "td/telegram/logevent/LogEvent.h" #include "td/telegram/net/NetQueryCreator.h" #include "td/telegram/Td.h" #include "td/telegram/TdDb.h" #include "td/telegram/telegram_api.h" #include "td/utils/algorithm.h" #include "td/utils/buffer.h" #include "td/utils/emoji.h" #include "td/utils/JsonBuilder.h" #include "td/utils/logging.h" #include "td/utils/Random.h" #include "td/utils/Time.h" #include "td/utils/tl_helpers.h" namespace td { class GetChatThemesQuery final : public Td::ResultHandler { Promise> promise_; public: explicit GetChatThemesQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(int64 hash) { send_query(G()->net_query_creator().create(telegram_api::account_getChatThemes(hash))); } 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(result_ptr.move_as_ok()); } void on_error(Status status) final { promise_.set_error(std::move(status)); } }; bool operator==(const ThemeManager::ThemeSettings &lhs, const ThemeManager::ThemeSettings &rhs) { return lhs.accent_color == rhs.accent_color && lhs.message_accent_color == rhs.message_accent_color && lhs.background_info == rhs.background_info && lhs.base_theme == rhs.base_theme && lhs.message_colors == rhs.message_colors && lhs.animate_message_colors == rhs.animate_message_colors; } bool operator!=(const ThemeManager::ThemeSettings &lhs, const ThemeManager::ThemeSettings &rhs) { return !(lhs == rhs); } template void ThemeManager::ThemeSettings::store(StorerT &storer) const { using td::store; bool has_message_accent_color = message_accent_color != accent_color; bool has_background = background_info.is_valid(); BEGIN_STORE_FLAGS(); STORE_FLAG(animate_message_colors); STORE_FLAG(has_message_accent_color); STORE_FLAG(has_background); END_STORE_FLAGS(); store(accent_color, storer); if (has_message_accent_color) { store(message_accent_color, storer); } if (has_background) { store(background_info, storer); } store(base_theme, storer); store(message_colors, storer); } template void ThemeManager::ThemeSettings::parse(ParserT &parser) { using td::parse; bool has_message_accent_color; bool has_background; BEGIN_PARSE_FLAGS(); PARSE_FLAG(animate_message_colors); PARSE_FLAG(has_message_accent_color); PARSE_FLAG(has_background); END_PARSE_FLAGS(); parse(accent_color, parser); if (has_message_accent_color) { parse(message_accent_color, parser); } else { message_accent_color = accent_color; } if (has_background) { parse(background_info, parser); } parse(base_theme, parser); parse(message_colors, parser); } template void ThemeManager::ChatTheme::store(StorerT &storer) const { BEGIN_STORE_FLAGS(); END_STORE_FLAGS(); td::store(emoji, storer); td::store(id, storer); td::store(light_theme, storer); td::store(dark_theme, storer); } template void ThemeManager::ChatTheme::parse(ParserT &parser) { BEGIN_PARSE_FLAGS(); END_PARSE_FLAGS(); td::parse(emoji, parser); td::parse(id, parser); td::parse(light_theme, parser); td::parse(dark_theme, parser); } template void ThemeManager::ChatThemes::store(StorerT &storer) const { td::store(hash, storer); td::store(themes, storer); } template void ThemeManager::ChatThemes::parse(ParserT &parser) { td::parse(hash, parser); td::parse(themes, parser); } template void ThemeManager::AccentColors::store(StorerT &storer) const { BEGIN_STORE_FLAGS(); END_STORE_FLAGS(); td::store(static_cast(light_colors_.size()), storer); for (auto &it : light_colors_) { td::store(it.first, storer); td::store(it.second, storer); } td::store(static_cast(dark_colors_.size()), storer); for (auto &it : dark_colors_) { td::store(it.first, storer); td::store(it.second, storer); } td::store(accent_color_ids_, storer); } template void ThemeManager::AccentColors::parse(ParserT &parser) { BEGIN_PARSE_FLAGS(); END_PARSE_FLAGS(); int32 size; td::parse(size, parser); for (int32 i = 0; i < size; i++) { AccentColorId accent_color_id; vector colors; td::parse(accent_color_id, parser); td::parse(colors, parser); CHECK(accent_color_id.is_valid()); light_colors_.emplace(accent_color_id, std::move(colors)); } td::parse(size, parser); for (int32 i = 0; i < size; i++) { AccentColorId accent_color_id; vector colors; td::parse(accent_color_id, parser); td::parse(colors, parser); CHECK(accent_color_id.is_valid()); dark_colors_.emplace(accent_color_id, std::move(colors)); } td::parse(accent_color_ids_, parser); } ThemeManager::ThemeManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) { load_accent_colors(); } void ThemeManager::start_up() { init(); } void ThemeManager::load_chat_themes() { // must not be called in constructor, because uses other managers if (!td_->auth_manager_->is_authorized() || td_->auth_manager_->is_bot()) { return; } auto log_event_string = G()->td_db()->get_binlog_pmc()->get(get_chat_themes_database_key()); if (!log_event_string.empty()) { auto status = log_event_parse(chat_themes_, log_event_string); if (status.is_ok()) { send_update_chat_themes(); } else { LOG(ERROR) << "Failed to parse chat themes from binlog: " << status; chat_themes_ = ChatThemes(); } } chat_themes_.next_reload_time = Time::now(); } void ThemeManager::load_accent_colors() { if (!td_->auth_manager_->is_authorized() || td_->auth_manager_->is_bot()) { return; } auto log_event_string = G()->td_db()->get_binlog_pmc()->get(get_accent_colors_database_key()); if (!log_event_string.empty()) { auto status = log_event_parse(accent_colors_, log_event_string); if (status.is_ok()) { send_update_accent_colors(); } else { LOG(ERROR) << "Failed to parse accent colors from binlog: " << status; accent_colors_ = AccentColors(); } } } void ThemeManager::init() { load_chat_themes(); loop(); } void ThemeManager::tear_down() { parent_.reset(); } void ThemeManager::loop() { if (!td_->auth_manager_->is_authorized() || td_->auth_manager_->is_bot()) { return; } if (Time::now() < chat_themes_.next_reload_time) { return set_timeout_at(chat_themes_.next_reload_time); } auto request_promise = PromiseCreator::lambda( [actor_id = actor_id(this)](Result> result) { send_closure(actor_id, &ThemeManager::on_get_chat_themes, std::move(result)); }); td_->create_handler(std::move(request_promise))->send(chat_themes_.hash); } bool ThemeManager::is_dark_base_theme(BaseTheme base_theme) { switch (base_theme) { case BaseTheme::Classic: case BaseTheme::Day: case BaseTheme::Arctic: return false; case BaseTheme::Night: case BaseTheme::Tinted: return true; default: UNREACHABLE(); return false; } } void ThemeManager::on_update_theme(telegram_api::object_ptr &&theme, Promise &&promise) { CHECK(theme != nullptr); bool is_changed = false; bool was_light = false; bool was_dark = false; for (auto &chat_theme : chat_themes_.themes) { if (chat_theme.id == theme->id_) { for (auto &settings : theme->settings_) { auto theme_settings = get_chat_theme_settings(std::move(settings)); if (theme_settings.message_colors.empty()) { continue; } if (is_dark_base_theme(theme_settings.base_theme)) { if (!was_dark) { was_dark = true; if (chat_theme.dark_theme != theme_settings) { chat_theme.dark_theme = std::move(theme_settings); is_changed = true; } } } else { if (!was_light) { was_light = true; if (chat_theme.light_theme != theme_settings) { chat_theme.light_theme = std::move(theme_settings); is_changed = true; } } } } } } if (is_changed) { save_chat_themes(); send_update_chat_themes(); } promise.set_value(Unit()); } void ThemeManager::on_update_accent_colors(FlatHashMap, AccentColorIdHash> light_colors, FlatHashMap, AccentColorIdHash> dark_colors, vector accent_color_ids) { auto are_equal = [](const FlatHashMap, AccentColorIdHash> &lhs, const FlatHashMap, AccentColorIdHash> &rhs) { for (auto &lhs_it : lhs) { auto rhs_it = rhs.find(lhs_it.first); if (rhs_it == rhs.end() || rhs_it->second != lhs_it.second) { return false; } } return true; }; if (accent_color_ids == accent_colors_.accent_color_ids_ && are_equal(light_colors, accent_colors_.light_colors_) && are_equal(dark_colors, accent_colors_.dark_colors_)) { return; } for (auto &it : light_colors) { accent_colors_.light_colors_[it.first] = std::move(it.second); } for (auto &it : dark_colors) { accent_colors_.dark_colors_[it.first] = std::move(it.second); } accent_colors_.accent_color_ids_ = std::move(accent_color_ids); save_accent_colors(); send_update_accent_colors(); } namespace { template static auto get_color_json(int32 color); template <> auto get_color_json(int32 color) { return static_cast(static_cast(color) | 0xFF000000); } template <> auto get_color_json(int32 color) { string res(7, '#'); const char *hex = "0123456789abcdef"; for (int i = 0; i < 3; i++) { int32 num = (color >> (i * 8)) & 0xFF; res[2 * i + 1] = hex[num >> 4]; res[2 * i + 2] = hex[num & 15]; } return res; } template string get_theme_parameters_json_string_impl(const td_api::object_ptr &theme) { if (for_web_view && theme == nullptr) { return "null"; } return json_encode(json_object([&theme](auto &o) { auto get_color = &get_color_json; o("bg_color", get_color(theme->background_color_)); o("secondary_bg_color", get_color(theme->secondary_background_color_)); o("text_color", get_color(theme->text_color_)); o("hint_color", get_color(theme->hint_color_)); o("link_color", get_color(theme->link_color_)); o("button_color", get_color(theme->button_color_)); o("button_text_color", get_color(theme->button_text_color_)); o("header_bg_color", get_color(theme->header_background_color_)); o("section_bg_color", get_color(theme->section_background_color_)); o("accent_text_color", get_color(theme->accent_text_color_)); o("section_header_text_color", get_color(theme->section_header_text_color_)); o("subtitle_text_color", get_color(theme->subtitle_text_color_)); o("destructive_text_color", get_color(theme->destructive_text_color_)); })); } } // namespace string ThemeManager::get_theme_parameters_json_string(const td_api::object_ptr &theme, bool for_web_view) { if (for_web_view) { return get_theme_parameters_json_string_impl(theme); } else { return get_theme_parameters_json_string_impl(theme); } } int32 ThemeManager::get_accent_color_id_object(AccentColorId accent_color_id, AccentColorId fallback_accent_color_id) const { CHECK(accent_color_id.is_valid()); if (td_->auth_manager_->is_bot() || accent_color_id.is_built_in() || accent_colors_.light_colors_.count(accent_color_id) != 0) { return accent_color_id.get(); } if (!fallback_accent_color_id.is_valid()) { return 5; // blue } CHECK(fallback_accent_color_id.is_built_in()); return fallback_accent_color_id.get(); } td_api::object_ptr ThemeManager::get_theme_settings_object(const ThemeSettings &settings) const { auto fill = [colors = settings.message_colors]() mutable -> td_api::object_ptr { if (colors.size() >= 3) { return td_api::make_object(std::move(colors)); } CHECK(!colors.empty()); if (colors.size() == 1 || colors[0] == colors[1]) { return td_api::make_object(colors[0]); } return td_api::make_object(colors[1], colors[0], 0); }(); // ignore settings.base_theme for now return td_api::make_object( settings.accent_color, settings.background_info.get_background_object(td_), std::move(fill), settings.animate_message_colors, settings.message_accent_color); } td_api::object_ptr ThemeManager::get_chat_theme_object(const ChatTheme &theme) const { return td_api::make_object(theme.emoji, get_theme_settings_object(theme.light_theme), get_theme_settings_object(theme.dark_theme)); } td_api::object_ptr ThemeManager::get_update_chat_themes_object() const { return td_api::make_object( transform(chat_themes_.themes, [this](const ChatTheme &theme) { return get_chat_theme_object(theme); })); } td_api::object_ptr ThemeManager::get_update_accent_colors_object() const { return accent_colors_.get_update_accent_colors_object(); } td_api::object_ptr ThemeManager::AccentColors::get_update_accent_colors_object() const { vector> colors; int32 base_colors[] = {0xDF2020, 0xDFA520, 0xA040A0, 0x208020, 0x20DFDF, 0x2044DF, 0xDF1493}; auto get_distance = [](int32 lhs_color, int32 rhs_color) { auto get_color_distance = [](int32 lhs, int32 rhs) { auto diff = max(lhs & 255, 0) - max(rhs & 255, 0); return diff * diff; }; return get_color_distance(lhs_color, rhs_color) + get_color_distance(lhs_color >> 8, rhs_color >> 8) + get_color_distance(lhs_color >> 16, rhs_color >> 16); }; for (auto &it : light_colors_) { auto light_colors = it.second; auto dark_it = dark_colors_.find(it.first); auto dark_colors = dark_it != dark_colors_.end() ? dark_it->second : light_colors; auto first_color = light_colors[0]; int best_index = 0; int32 best_distance = get_distance(base_colors[0], first_color); for (int i = 1; i < 7; i++) { auto cur_distance = get_distance(base_colors[i], first_color); if (cur_distance < best_distance) { best_distance = cur_distance; best_index = i; } } colors.push_back(td_api::make_object(it.first.get(), best_index, std::move(light_colors), std::move(dark_colors))); } auto available_accent_color_ids = transform(accent_color_ids_, [](AccentColorId accent_color_id) { return accent_color_id.get(); }); return td_api::make_object(std::move(colors), std::move(available_accent_color_ids)); } string ThemeManager::get_chat_themes_database_key() { return "chat_themes"; } string ThemeManager::get_accent_colors_database_key() { return "accent_colors"; } void ThemeManager::save_chat_themes() { G()->td_db()->get_binlog_pmc()->set(get_chat_themes_database_key(), log_event_store(chat_themes_).as_slice().str()); } void ThemeManager::save_accent_colors() { G()->td_db()->get_binlog_pmc()->set(get_accent_colors_database_key(), log_event_store(accent_colors_).as_slice().str()); } void ThemeManager::send_update_chat_themes() const { send_closure(G()->td(), &Td::send_update, get_update_chat_themes_object()); } void ThemeManager::send_update_accent_colors() const { send_closure(G()->td(), &Td::send_update, get_update_accent_colors_object()); } void ThemeManager::on_get_chat_themes(Result> result) { if (result.is_error()) { set_timeout_in(Random::fast(40, 60)); return; } chat_themes_.next_reload_time = Time::now() + THEME_CACHE_TIME; set_timeout_at(chat_themes_.next_reload_time); auto chat_themes_ptr = result.move_as_ok(); LOG(DEBUG) << "Receive " << to_string(chat_themes_ptr); if (chat_themes_ptr->get_id() == telegram_api::account_themesNotModified::ID) { return; } CHECK(chat_themes_ptr->get_id() == telegram_api::account_themes::ID); auto chat_themes = telegram_api::move_object_as(chat_themes_ptr); chat_themes_.hash = chat_themes->hash_; chat_themes_.themes.clear(); for (auto &theme : chat_themes->themes_) { if (!is_emoji(theme->emoticon_) || !theme->for_chat_) { LOG(ERROR) << "Receive " << to_string(theme); continue; } bool was_light = false; bool was_dark = false; ChatTheme chat_theme; chat_theme.emoji = std::move(theme->emoticon_); chat_theme.id = theme->id_; for (auto &settings : theme->settings_) { auto theme_settings = get_chat_theme_settings(std::move(settings)); if (theme_settings.message_colors.empty()) { continue; } if (is_dark_base_theme(theme_settings.base_theme)) { if (!was_dark) { was_dark = true; if (chat_theme.dark_theme != theme_settings) { chat_theme.dark_theme = std::move(theme_settings); } } } else { if (!was_light) { was_light = true; if (chat_theme.light_theme != theme_settings) { chat_theme.light_theme = std::move(theme_settings); } } } } if (chat_theme.light_theme.message_colors.empty() || chat_theme.dark_theme.message_colors.empty()) { continue; } chat_themes_.themes.push_back(std::move(chat_theme)); } save_chat_themes(); send_update_chat_themes(); } ThemeManager::BaseTheme ThemeManager::get_base_theme( const telegram_api::object_ptr &base_theme) { CHECK(base_theme != nullptr); switch (base_theme->get_id()) { case telegram_api::baseThemeClassic::ID: return BaseTheme::Classic; case telegram_api::baseThemeDay::ID: return BaseTheme::Day; case telegram_api::baseThemeNight::ID: return BaseTheme::Night; case telegram_api::baseThemeTinted::ID: return BaseTheme::Tinted; case telegram_api::baseThemeArctic::ID: return BaseTheme::Arctic; default: UNREACHABLE(); return BaseTheme::Classic; } } ThemeManager::ThemeSettings ThemeManager::get_chat_theme_settings( telegram_api::object_ptr settings) { ThemeSettings result; if (settings != nullptr && !settings->message_colors_.empty() && settings->message_colors_.size() <= 4) { result.accent_color = settings->accent_color_; bool has_outbox_accent_color = (settings->flags_ & telegram_api::themeSettings::OUTBOX_ACCENT_COLOR_MASK) != 0; result.message_accent_color = (has_outbox_accent_color ? settings->outbox_accent_color_ : result.accent_color); result.background_info = BackgroundInfo(td_, std::move(settings->wallpaper_)); result.base_theme = get_base_theme(settings->base_theme_); result.message_colors = std::move(settings->message_colors_); result.animate_message_colors = settings->message_colors_animated_; } return result; } void ThemeManager::get_current_state(vector> &updates) const { if (!td_->auth_manager_->is_authorized() || td_->auth_manager_->is_bot()) { return; } if (!chat_themes_.themes.empty()) { updates.push_back(get_update_chat_themes_object()); } if (!accent_colors_.accent_color_ids_.empty()) { updates.push_back(get_update_accent_colors_object()); } } } // namespace td