From 8a1fdf29431b8d025de37df8d17aa0ab8ca4519a Mon Sep 17 00:00:00 2001 From: bleizix Date: Sun, 14 Sep 2025 23:24:36 +0500 Subject: [PATCH] feat(WIP): message filters --- Telegram/CMakeLists.txt | 14 + Telegram/Resources/icons/ayu/add.png | Bin 0 -> 236 bytes Telegram/Resources/icons/ayu/add@2x.png | Bin 0 -> 310 bytes Telegram/Resources/icons/ayu/add@3x.png | Bin 0 -> 400 bytes Telegram/Resources/langs/lang.strings | 2 + Telegram/SourceFiles/ayu/ayu_infra.cpp | 5 + Telegram/SourceFiles/ayu/ayu_settings.cpp | 41 ++ Telegram/SourceFiles/ayu/ayu_settings.h | 18 + .../SourceFiles/ayu/data/ayu_database.cpp | 162 +++++- Telegram/SourceFiles/ayu/data/ayu_database.h | 24 + Telegram/SourceFiles/ayu/data/entities.h | 27 +- .../filters/filters_cache_controller.cpp | 143 +++++ .../filters/filters_cache_controller.h | 29 + .../features/filters/filters_controller.cpp | 188 +++++++ .../ayu/features/filters/filters_controller.h | 52 ++ .../ayu/features/filters/filters_utils.cpp | 509 ++++++++++++++++++ .../ayu/features/filters/filters_utils.h | 83 +++ .../ayu/features/filters/shadow_ban_utils.cpp | 64 +++ .../ayu/features/filters/shadow_ban_utils.h | 24 + Telegram/SourceFiles/ayu/ui/ayu_icons.style | 13 + .../ayu/ui/context_menu/context_menu.cpp | 28 + .../ayu/ui/context_menu/context_menu.h | 2 + .../ayu/ui/settings/filters/edit_filter.cpp | 434 +++++++++++++++ .../ayu/ui/settings/filters/edit_filter.h | 43 ++ .../filters/peer_global_exclusion.cpp | 169 ++++++ .../settings/filters/peer_global_exclusion.h | 71 +++ .../filters/settings_filters_list.cpp | 276 ++++++++++ .../settings/filters/settings_filters_list.h | 52 ++ .../ayu/ui/settings/settings_ayu.cpp | 124 ++++- .../ayu/utils/telegram_helpers.cpp | 64 ++- .../SourceFiles/ayu/utils/telegram_helpers.h | 5 +- Telegram/SourceFiles/boxes/peer_list_box.h | 6 + .../history/history_inner_widget.cpp | 14 + .../history/history_item_components.cpp | 11 +- .../SourceFiles/history/history_widget.cpp | 6 +- .../history/view/history_view_element.cpp | 2 +- .../history/view/history_view_send_action.cpp | 5 +- .../SourceFiles/info/info_wrap_widget.cpp | 59 ++ .../SourceFiles/window/window_peer_menu.cpp | 1 + .../window/window_session_controller.h | 4 + 40 files changed, 2746 insertions(+), 28 deletions(-) create mode 100644 Telegram/Resources/icons/ayu/add.png create mode 100644 Telegram/Resources/icons/ayu/add@2x.png create mode 100644 Telegram/Resources/icons/ayu/add@3x.png create mode 100644 Telegram/SourceFiles/ayu/features/filters/filters_cache_controller.cpp create mode 100644 Telegram/SourceFiles/ayu/features/filters/filters_cache_controller.h create mode 100644 Telegram/SourceFiles/ayu/features/filters/filters_controller.cpp create mode 100644 Telegram/SourceFiles/ayu/features/filters/filters_controller.h create mode 100644 Telegram/SourceFiles/ayu/features/filters/filters_utils.cpp create mode 100644 Telegram/SourceFiles/ayu/features/filters/filters_utils.h create mode 100644 Telegram/SourceFiles/ayu/features/filters/shadow_ban_utils.cpp create mode 100644 Telegram/SourceFiles/ayu/features/filters/shadow_ban_utils.h create mode 100644 Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.cpp create mode 100644 Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.h create mode 100644 Telegram/SourceFiles/ayu/ui/settings/filters/peer_global_exclusion.cpp create mode 100644 Telegram/SourceFiles/ayu/ui/settings/filters/peer_global_exclusion.h create mode 100644 Telegram/SourceFiles/ayu/ui/settings/filters/settings_filters_list.cpp create mode 100644 Telegram/SourceFiles/ayu/ui/settings/filters/settings_filters_list.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index f843a5eb23..8421877be4 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -148,6 +148,12 @@ set(ayugram_files ayu/ui/settings/settings_main.h ayu/ui/settings/settings_other.cpp ayu/ui/settings/settings_other.h + ayu/ui/settings/filters/settings_filters_list.cpp + ayu/ui/settings/filters/settings_filters_list.h + ayu/ui/settings/filters/peer_global_exclusion.cpp + ayu/ui/settings/filters/peer_global_exclusion.h + ayu/ui/settings/filters/edit_filter.cpp + ayu/ui/settings/filters/edit_filter.h ayu/ui/context_menu/context_menu.cpp ayu/ui/context_menu/context_menu.h ayu/ui/context_menu/menu_item_subtext.cpp @@ -205,6 +211,14 @@ set(ayugram_files ayu/features/translator/implementations/telegram.h ayu/features/translator/implementations/base.cpp ayu/features/translator/implementations/base.h + ayu/features/filters/filters_controller.cpp + ayu/features/filters/filters_controller.h + ayu/features/filters/filters_cache_controller.cpp + ayu/features/filters/filters_cache_controller.h + ayu/features/filters/filters_utils.cpp + ayu/features/filters/filters_utils.h + ayu/features/filters/shadow_ban_utils.cpp + ayu/features/filters/shadow_ban_utils.h ayu/data/messages_storage.cpp ayu/data/messages_storage.h ayu/data/entities.h diff --git a/Telegram/Resources/icons/ayu/add.png b/Telegram/Resources/icons/ayu/add.png new file mode 100644 index 0000000000000000000000000000000000000000..b4f18d5de4348c773ee5937c33ce272db2caf95b GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1SFYWcSQjy#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyy~SS-^~7gA{JM`{6H;3iEVv46zVQ zPLN;?;ci@;;IORl|3QZSG&P$dmZd9JNxtyy$Zk|Ql4c>U(fUkGD5p)jJ&mPI?}*=< zqJ%)9oMR@fN@sOuJUi&RE%&zDEc=I6?vgj085q8J>vg;M8O#RR|OC BI<)`* literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/ayu/add@2x.png b/Telegram/Resources/icons/ayu/add@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d40c899cc2bb553f03f7637fd564b646089f29a8 GIT binary patch literal 310 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1SFZ~=vx6P#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93uP|pHJ24*152ohVs1eeuVz>Hvn6jnV;TLYvPc)B=-L~y>H z63N$~z~i!V`{RG^6RIMsORdW<3v6&;U~zb$&XUwq9jCJ_Zf3V! z<;D-9YVi`{R~?l6C;2{B=N6gA_Gk5qqw62Z{N9rAW?#m)8`1TvgH z?hExRXJ4BIR=4q)pJ?6c>!aryD0;e1Ph?%)^r9I-lf*@z=ByB^FPG?b-tAH(z`!W* b!utWsft?3e%F2i9f!yrr>gTe~DWM4fFbcUp>O_%BREMXF>6|wLXN($dq%L%1U=nCxU{PRTh9+*1FF=HB-{-F09-${qWz;kD^t-j6X)yC)FI4Tv(N}+CI}-?)cjM zPrYZqnDd!qYV)bOCI5;9zwWE={jIlXbLNzzodKJrBQBdy*k+t7aBr&8FRf1#+Dcj( zjwd$FVdQ&MBb@0OIJ7WB>pF literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c907aad5d9..d89251b245 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7000,6 +7000,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "ayu_FiltersHideFromBlockedNote" = "Message filters were enabled."; "ayu_RegexFiltersEnableSharedInChats" = "Enable Shared Filters in Chats"; "ayu_RegexFiltersAdd" = "Add Filter"; +"ayu_RegexFiltersAddDescription" = "You can use {link} to test and debug your regular expression."; +"ayu_RegexFiltersAddDescription_link" = "regex101.com"; "ayu_RegexFiltersEdit" = "Edit Filter"; "ayu_RegexFiltersPlaceholder" = "Expression"; "ayu_EnableExpression" = "Enable Filter"; diff --git a/Telegram/SourceFiles/ayu/ayu_infra.cpp b/Telegram/SourceFiles/ayu/ayu_infra.cpp index 129d1e1fd0..8af3a1450f 100644 --- a/Telegram/SourceFiles/ayu/ayu_infra.cpp +++ b/Telegram/SourceFiles/ayu/ayu_infra.cpp @@ -12,6 +12,7 @@ #include "ayu/ayu_worker.h" #include "ayu/data/ayu_database.h" #include "features/translator/ayu_translator.h" +#include "features/filters/shadow_ban_utils.h" #include "lang/lang_instance.h" #include "utils/rc_manager.h" @@ -52,12 +53,16 @@ void initTranslator() { Ayu::Translator::TranslateManager::init(); } +void initShadowBan() { + ShadowBanUtils::reloadShadowBan(); +} void init() { initLang(); initDatabase(); initUiSettings(); initWorker(); initRCManager(); + initShadowBan(); initTranslator(); } diff --git a/Telegram/SourceFiles/ayu/ayu_settings.cpp b/Telegram/SourceFiles/ayu/ayu_settings.cpp index 332b9489bf..632ffc5ce1 100644 --- a/Telegram/SourceFiles/ayu/ayu_settings.cpp +++ b/Telegram/SourceFiles/ayu/ayu_settings.cpp @@ -44,7 +44,13 @@ rpl::variable showPeerIdReactive; rpl::variable translationProviderReactive; +rpl::variable filtersEnabledReactive; +rpl::variable filtersEnabledInChatsReactive; +rpl::variable shadowBanIdsReactive; rpl::variable hideFromBlockedReactive; + +rpl::event_stream<> filtersUpdateReactive; // triggered on adding / editing filter + rpl::event_stream<> historyUpdateReactive; rpl::lifetime lifetime = rpl::lifetime(); @@ -142,6 +148,9 @@ void postinitialize() { showPeerIdReactive = settings->showPeerId; translationProviderReactive = settings->translationProvider; + filtersEnabledReactive = settings->filtersEnabled; + filtersEnabledInChatsReactive = settings->filtersEnabledInChats; + shadowBanIdsReactive = settings->shadowBanIds; hideFromBlockedReactive = settings->hideFromBlocked; ghostModeEnabled = ghostModeEnabled_util(settings.value()); @@ -226,6 +235,8 @@ AyuGramSettings::AyuGramSettings() { saveForBots = false; // ~ Message filters + filtersEnabled = false; + filtersEnabledInChats = false; hideFromBlocked = false; // ~ QoL toggles @@ -397,6 +408,20 @@ void set_saveForBots(bool val) { settings->saveForBots = val; } +void set_filtersEnabled(bool val) { + settings->filtersEnabled = val; + filtersEnabledReactive = val; +} +void set_filtersEnabledInChats(bool val) { + settings->filtersEnabledInChats = val; + filtersEnabledInChatsReactive = val; +} + +void set_shadowBanIds(const QString &val) { + settings->shadowBanIds = val; + shadowBanIdsReactive = val; +} + void set_hideFromBlocked(bool val) { settings->hideFromBlocked = val; hideFromBlockedReactive = val; @@ -694,9 +719,25 @@ rpl::producer get_ghostModeEnabledReactive() { return ghostModeEnabled.value(); } +rpl::producer get_filtersEnabledReactive() { + return filtersEnabledReactive.value(); +} +rpl::producer get_filtersEnabledInChatsReactive() { + return filtersEnabledInChatsReactive.value(); +} +rpl::producer get_shadowBanIdsReactive() { + return shadowBanIdsReactive.value(); +} + rpl::producer get_hideFromBlockedReactive() { return hideFromBlockedReactive.value(); } +void fire_filtersUpdate() { + filtersUpdateReactive.fire({}); +} +rpl::producer<> get_filtersUpdate() { + return filtersUpdateReactive.events(); +} void triggerHistoryUpdate() { historyUpdateReactive.fire({}); diff --git a/Telegram/SourceFiles/ayu/ayu_settings.h b/Telegram/SourceFiles/ayu/ayu_settings.h index cd04ad5149..9023dc80c2 100644 --- a/Telegram/SourceFiles/ayu/ayu_settings.h +++ b/Telegram/SourceFiles/ayu/ayu_settings.h @@ -59,6 +59,9 @@ public: bool saveForBots; + QString shadowBanIds; + bool filtersEnabled; + bool filtersEnabledInChats; bool hideFromBlocked; bool disableAds; @@ -160,6 +163,9 @@ void set_saveMessagesHistory(bool val); void set_saveForBots(bool val); +void set_filtersEnabled(bool val); +void set_filtersEnabledInChats(bool val); +void set_shadowBanIds(const QString &val); void set_hideFromBlocked(bool val); void set_disableAds(bool val); @@ -255,6 +261,9 @@ inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nloh NLOHMANN_JSON_TO(saveDeletedMessages) NLOHMANN_JSON_TO(saveMessagesHistory) NLOHMANN_JSON_TO(saveForBots) + NLOHMANN_JSON_TO(shadowBanIds) + NLOHMANN_JSON_TO(filtersEnabled) + NLOHMANN_JSON_TO(filtersEnabledInChats) NLOHMANN_JSON_TO(hideFromBlocked) NLOHMANN_JSON_TO(disableAds) NLOHMANN_JSON_TO(disableStories) @@ -334,6 +343,9 @@ inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nl NLOHMANN_JSON_FROM_WITH_DEFAULT(saveDeletedMessages) NLOHMANN_JSON_FROM_WITH_DEFAULT(saveMessagesHistory) NLOHMANN_JSON_FROM_WITH_DEFAULT(saveForBots) + NLOHMANN_JSON_FROM_WITH_DEFAULT(filtersEnabled) + NLOHMANN_JSON_FROM_WITH_DEFAULT(filtersEnabledInChats) + NLOHMANN_JSON_FROM_WITH_DEFAULT(shadowBanIds) NLOHMANN_JSON_FROM_WITH_DEFAULT(hideFromBlocked) NLOHMANN_JSON_FROM_WITH_DEFAULT(disableAds) NLOHMANN_JSON_FROM_WITH_DEFAULT(disableStories) @@ -418,8 +430,14 @@ bool isUseScheduledMessages(); rpl::producer get_ghostModeEnabledReactive(); +rpl::producer get_filtersEnabledReactive(); +rpl::producer get_filtersEnabledInChatsReactive(); +rpl::producer get_shadowBanIdsReactive(); rpl::producer get_hideFromBlockedReactive(); +void fire_filtersUpdate(); +rpl::producer<> get_filtersUpdate(); + void triggerHistoryUpdate(); rpl::producer<> get_historyUpdateReactive(); diff --git a/Telegram/SourceFiles/ayu/data/ayu_database.cpp b/Telegram/SourceFiles/ayu/data/ayu_database.cpp index 3631831ab7..55f409aed2 100644 --- a/Telegram/SourceFiles/ayu/data/ayu_database.cpp +++ b/Telegram/SourceFiles/ayu/data/ayu_database.cpp @@ -111,7 +111,7 @@ auto storage = make_storage( ), make_table( "RegexFilter", - make_column("id", &RegexFilter::id), + make_column("id", &RegexFilter::id, primary_key()), make_column("text", &RegexFilter::text), make_column("enabled", &RegexFilter::enabled), make_column("reversed", &RegexFilter::reversed), @@ -276,5 +276,165 @@ bool hasDeletedMessages(ID userId, ID dialogId, ID topicId) { return false; } } +template +std::vector getAllT() { + try { + return storage.get_all(); + } catch (std::exception &ex) { + LOG(("Failed to get all: %1").arg(ex.what())); + return {}; + } +} + +std::vector getAllRegexFilters() { + return getAllT(); +} + +std::vector getAllFiltersExclusions() { + return getAllT(); +} + +std::vector getExcludedByDialogId(ID dialogId) { + try { + return storage.get_all( + where(in(&RegexFilter::id, + storage.select(columns(&RegexFilterGlobalExclusion::filterId), + where(is_equal(&RegexFilterGlobalExclusion::dialogId, dialogId)) + ) + )) + ); + } catch (std::exception &ex) { + LOG(("Failed to get excluded by dialog id: %1").arg(ex.what())); + return {}; + } +} + +int getCount() { + try { + return storage.count(); + } catch (std::exception &ex) { + LOG(("Failed to get count: %1").arg(ex.what())); + return 0; + } +} + +RegexFilter getById(std::vector id) { + try { + return storage.get( + where(column(&RegexFilter::id) == std::move(id)) + ); + } catch (std::exception &ex) { + LOG(("Failed to get filters by id: %1").arg(ex.what())); + return {}; + } +} + +std::vector getShared() { + try { + return storage.get_all( + where(is_null(column(&RegexFilter::dialogId))) + ); + } catch (std::exception &ex) { + LOG(("Failed to get shared filters: %1").arg(ex.what())); + return {}; + } +} + +std::vector getByDialogId(ID dialogId) { + try { + return storage.get_all( + where(column(&RegexFilter::dialogId) == dialogId) + ); + } catch (std::exception &ex) { + LOG(("Failed to get filters by dialog id: %1").arg(ex.what())); + return {}; + } +} + + +void addRegexFilter(const RegexFilter &filter) { + try { + storage.begin_transaction(); + storage.replace(filter); // we're using replace as we set std::vector as primary key + storage.commit(); + } catch (std::exception &ex) { + LOG(("Failed to save regex filter for some reason: %1").arg(ex.what())); + } +} + +void addRegexExclusion(const RegexFilterGlobalExclusion &exclusion) { + try { + storage.begin_transaction(); + storage.insert(exclusion); + storage.commit(); + } catch (std::exception &ex) { + LOG(("Failed to save regex filter exclusion for some reason: %1").arg(ex.what())); + } +} + +void updateRegexFilter(const RegexFilter &filter) { + try { + storage.update_all( + set( + c(&RegexFilter::text) = filter.text, + c(&RegexFilter::enabled) = filter.enabled, + c(&RegexFilter::reversed) = filter.reversed, + c(&RegexFilter::caseInsensitive) = filter.caseInsensitive, + c(&RegexFilter::dialogId) = filter.dialogId + ), + where(c(&RegexFilter::id) == filter.id) + ); + } catch (std::exception &ex) { + LOG(("Failed to update regex filter for some reason: %1").arg(ex.what())); + } +} + +void deleteFilter(const std::vector &id) { + try { + storage.remove_all( + where(column(&RegexFilter::id) == id) + ); + } catch (std::exception &ex) { + LOG(("Failed to delete regex filter for some reason: %1").arg(ex.what())); + } +} + +void deleteExclusionsByFilterId(const std::vector &id) { + try { + storage.remove_all( + where(column(&RegexFilterGlobalExclusion::filterId) == id) + ); + } catch (std::exception &ex) { + LOG(("Failed to delete regex filter exclusion by filter id for some reason: %1").arg(ex.what())); + } +} + +void deleteExclusion(ID dialogId, std::vector filterId) { + try { + storage.remove_all( + where(column(&RegexFilterGlobalExclusion::filterId) == filterId and + column(&RegexFilterGlobalExclusion::dialogId) == dialogId + ) + ); + } catch (std::exception &ex) { + LOG(("Failed to delete regex filter exclusion for some reason: %1").arg(ex.what())); + } +} + +void deleteAllFilters() { + try { + storage.remove_all(); + } catch (std::exception &ex) { + LOG(("Failed to delete all regex filter for some reason: %1").arg(ex.what())); + } +} + +void deleteAllExclusions() { + try { + storage.remove_all(); + } catch (std::exception &ex) { + LOG(("Failed to delete all regex filter exclusions for some reason: %1").arg(ex.what())); + } +} } diff --git a/Telegram/SourceFiles/ayu/data/ayu_database.h b/Telegram/SourceFiles/ayu/data/ayu_database.h index 417113ba5a..a5ecf3dd1a 100644 --- a/Telegram/SourceFiles/ayu/data/ayu_database.h +++ b/Telegram/SourceFiles/ayu/data/ayu_database.h @@ -20,4 +20,28 @@ void addDeletedMessage(const DeletedMessage &message); std::vector getDeletedMessages(ID userId, ID dialogId, ID topicId, ID minId, ID maxId, int totalLimit); bool hasDeletedMessages(ID userId, ID dialogId, ID topicId); +std::vector getAllRegexFilters(); +RegexFilter getById(std::vector id); +std::vector getShared(); +std::vector getByDialogId(ID dialogId); +std::vector getAllFiltersExclusions(); +std::vector getExcludedByDialogId(ID dialogId); + +int getCount(); + + +void addRegexFilter(const RegexFilter &filter); +void addRegexExclusion(const RegexFilterGlobalExclusion &exclusion); + +void updateRegexFilter(const RegexFilter &filter); + +void deleteFilter(const std::vector &id); +void deleteExclusionsByFilterId(const std::vector &id); +void deleteExclusion(ID dialogId, std::vector filterId); + +void deleteAllFilters(); +void deleteAllExclusions(); + + + } diff --git a/Telegram/SourceFiles/ayu/data/entities.h b/Telegram/SourceFiles/ayu/data/entities.h index 52e470b698..6178006798 100644 --- a/Telegram/SourceFiles/ayu/data/entities.h +++ b/Telegram/SourceFiles/ayu/data/entities.h @@ -80,7 +80,28 @@ public: bool enabled; bool reversed; bool caseInsensitive; - std::unique_ptr dialogId; // nullable + std::optional dialogId; // nullable + + bool operator==(const RegexFilter &other) const { + return id == other.id && + text == other.text && + caseInsensitive == other.caseInsensitive && + reversed == other.reversed && + dialogId == other.dialogId && + enabled == other.enabled; + } + [[nodiscard]] QJsonObject toJson() const { + QJsonObject json; + json["id"] = QString::fromUtf8(id); + json["text"] = QString::fromStdString(text); + json["enabled"] = enabled; + json["reversed"] = reversed; + json["caseInsensitive"] = caseInsensitive; + if (dialogId.has_value()) { + json["dialogId"] = dialogId.value(); + } + return json; + } }; class RegexFilterGlobalExclusion @@ -89,6 +110,10 @@ public: ID fakeId; ID dialogId; std::vector filterId; + + bool operator==(const RegexFilterGlobalExclusion& other) const { + return dialogId == other.dialogId && filterId == other.filterId; + } }; class SpyMessageRead diff --git a/Telegram/SourceFiles/ayu/features/filters/filters_cache_controller.cpp b/Telegram/SourceFiles/ayu/features/filters/filters_cache_controller.cpp new file mode 100644 index 0000000000..270dc6cb70 --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/filters/filters_cache_controller.cpp @@ -0,0 +1,143 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#include "filters_cache_controller.h" + +#include + +#include "filters_controller.h" +#include "ayu/data/ayu_database.h" +#include "data/data_peer.h" +#include "history/history.h" +#include "history/history_item.h" + +static std::mutex mutex; + +namespace FiltersCacheController { +std::optional> sharedPatterns; +std::optional>> patternsByDialogId; + +std::optional>> exclusionsByDialogId; + +std::unordered_map,std::optional>> filteredMessages; + + +void rebuildCache() { + std::lock_guard lock(mutex); + + const auto filters = AyuDatabase::getAllRegexFilters(); + const auto exclusions = AyuDatabase::getAllFiltersExclusions(); + + std::vector shared; + std::unordered_map> byDialogId; + + for (const auto &filter : filters) { + if (!filter.enabled || filter.text.empty()) { + continue; + } + + int flags = UREGEX_MULTILINE; + if (filter.caseInsensitive) + flags |= UREGEX_CASE_INSENSITIVE; + + + auto status = U_ZERO_ERROR; + auto pattern = RegexPattern::compile(UnicodeString::fromUTF8(filter.text), flags, status); + + if (!pattern) { + continue; + } + + if (filter.dialogId.has_value()) { + byDialogId[filter.dialogId.value()].push_back({std::shared_ptr(pattern), filter.reversed}); + } else { + shared.push_back({filter.id, {std::shared_ptr(pattern), filter.reversed}}); + } + } + + auto exclByDialogId = buildExclusions(exclusions, shared); + + sharedPatterns = shared; + patternsByDialogId = byDialogId; + exclusionsByDialogId = exclByDialogId; + filteredMessages.clear(); +} + +std::unordered_map> buildExclusions( + const std::vector &exclusions, + const std::vector &shared) { + + std::unordered_map> exclusionsByDialogId; + + for (const auto &exclusion : exclusions) { + + auto &exclusionSet = exclusionsByDialogId[exclusion.dialogId]; + + for (const auto &filter : shared) { + if (filter.id == exclusion.filterId) { + + exclusionSet.insert(filter); + break; + } + } + } + return exclusionsByDialogId; +} + +std::optional isFiltered(not_null item) { + std::lock_guard lock(mutex); + auto dialogIt = filteredMessages.find(item->history()->peer->id.value); + + if (dialogIt == filteredMessages.end()) { + return std::nullopt; + } + + const auto it = dialogIt->second.find(item->id.bare); + if (it == dialogIt->second.end()) { + return std::nullopt; + } + + return it->second; +} + +void putFiltered(not_null item, bool res) { + std::lock_guard lock(mutex); + filteredMessages[item->history()->peer->id.value][item->id.bare] = res; +} + +std::optional> getPatternsByDialogId(uint64 dialogId) { + if (!patternsByDialogId.has_value()) { + rebuildCache(); + } + const auto it = patternsByDialogId.value().find(dialogId); + if (it == patternsByDialogId.value().end()) { + return std::nullopt; + } + + return it->second; +} + +std::optional> getExclusionsByDialogId(long long dialogId) { + std::lock_guard lock(mutex); + + if (!exclusionsByDialogId.has_value()) { + rebuildCache(); + } + const auto it = exclusionsByDialogId.value().find(dialogId); + if (it == exclusionsByDialogId.value().end()) { + return std::nullopt; + } + return it->second; +} + +const std::vector &getSharedPatterns() { + if (!sharedPatterns.has_value()) { + rebuildCache(); + } + return sharedPatterns.value(); +} + +} diff --git a/Telegram/SourceFiles/ayu/features/filters/filters_cache_controller.h b/Telegram/SourceFiles/ayu/features/filters/filters_cache_controller.h new file mode 100644 index 0000000000..961180a298 --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/filters/filters_cache_controller.h @@ -0,0 +1,29 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#include "filters_controller.h" +#include "ayu/data/entities.h" + +using namespace FiltersController; + +namespace FiltersCacheController { + + +void rebuildCache(); + +std::unordered_map> buildExclusions( + const std::vector& exclusions, + const std::vector& shared); + +std::optional isFiltered(not_null item); +void putFiltered(not_null item, bool res); + +std::optional> getPatternsByDialogId(uint64 dialogId); +std::optional> getExclusionsByDialogId(long long dialogId); +const std::vector &getSharedPatterns(); + +} + diff --git a/Telegram/SourceFiles/ayu/features/filters/filters_controller.cpp b/Telegram/SourceFiles/ayu/features/filters/filters_controller.cpp new file mode 100644 index 0000000000..7f1e12958c --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/filters/filters_controller.cpp @@ -0,0 +1,188 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 + +#include "filters_controller.h" + +#include "filters_cache_controller.h" +#include "ayu/ayu_settings.h" +#include "data/data_peer.h" +#include "data/data_user.h" +#include "history/history.h" +#include "history/history_item.h" +#include "unicode/regex.h" + +#include +#include + +#include "apiwrap.h" + +#include "ayu/data/entities.h" +#include "core/mime_type.h" +#include "data/data_channel.h" +#include "data/data_peer_id.h" + +#include "data/data_session.h" +#include "history/history_item_components.h" + +#include "filters_utils.h" +#include "shadow_ban_utils.h" + +namespace FiltersController { + +bool filterBlocked(const not_null item) { + if (item->from() != item->history()->peer) { + if (isBlocked(item)) { + return true; + } + } + return false; +} + +std::optional isFiltered(const QString &str, uint64 dialogId) { + if (str.isEmpty()) { + return std::nullopt; + } + + auto icuStr = UnicodeString(reinterpret_cast(str.constData()), str.length()); + + const auto matches = [&](const ReversiblePattern &pattern) + { + UErrorCode status = U_ZERO_ERROR; + + auto match = pattern.pattern->matcher(icuStr, status)->find(); + if (U_FAILURE(status)) { + LOG(("FILTER FAILED: %1").arg(u_errorName(status))); + return false; + } + + const auto reversed = pattern.reversed; + + if (!reversed && match || reversed && !match) { + return true; + } + return false; + }; + + if (const auto &dialogPatterns = FiltersCacheController::getPatternsByDialogId(dialogId); + dialogPatterns.has_value() && !dialogPatterns.value().empty()) { + + for (const auto &pattern : dialogPatterns.value()) { + return matches(pattern); + } + } + + const auto &exclusions = FiltersCacheController::getExclusionsByDialogId(dialogId); + if (const auto &sharedPatterns = FiltersCacheController::getSharedPatterns(); !sharedPatterns.empty()) { + for (const auto &pattern : sharedPatterns) { + if (exclusions.has_value() && exclusions.value().contains(pattern)) { + continue; + } + if (matches(pattern.pattern)) { + return true; + } + } + } + return false; +} + +bool isEnabled(not_null peer) { + auto &settings = AyuSettings::getInstance(); + return settings.filtersEnabled && (settings.filtersEnabledInChats || peer->asChannel()); +} + +bool isBlocked(const not_null item) { + auto &settings = AyuSettings::getInstance(); + + ID peer = 0; + if (const auto user = item->from()->asUser()) { + peer = user->id.value & PeerId::kChatTypeMask; + } + + const auto blocked = [&]() -> bool + { + if (item->from()->isUser() && + item->from()->asUser()->isBlocked()) { + // don't hide messages if it's a dialog with blocked user + return item->from()->asUser()->id != item->history()->peer->id; + } + + if (const auto forwarded = item->Get()) { + if (forwarded->originalSender && + forwarded->originalSender->isUser() && + forwarded->originalSender->asUser()->isBlocked()) { + return true; + } + } + return false; + }(); + + return settings.filtersEnabled && + ( + ShadowBanUtils::isShadowBanned(peer) || + settings.hideFromBlocked && blocked + ); +} + +// unused, probably need to remove +bool filteredWithoutCaching(const not_null item) { + auto &settings = AyuSettings::getInstance(); + if (!settings.filtersEnabled) { + return false; + } + + if (item->out()) { + return false; + } + + if (filterBlocked(item)) return true; + + if (!isEnabled(item->from())) return false; + + const auto cached = FiltersCacheController::isFiltered(item); + if (cached.has_value()) { + return cached.value(); + } + const auto filtered = isFiltered(FilterUtils::extractAllText(item), item->id.bare); + if (filtered.has_value()) { + return filtered.value(); + } + return false; +} + +// Main Method +bool filtered(const not_null item) { + auto &settings = AyuSettings::getInstance(); + + if (!settings.filtersEnabled) { + return false; + } + + if (item->out()) { + return false; + } + + if (filterBlocked(item)) return true; + + if (!isEnabled(item->history()->peer)) return false; + + const auto cached = FiltersCacheController::isFiltered(item); + if (cached.has_value()) { + return cached.value(); + } + const auto res = isFiltered(FilterUtils::extractAllText(item), item->history()->peer->id.value & PeerId::kChatTypeMask); + + // sometimes item has empty text. + // so we cache result only if + // processed item is filterable + if (res.has_value()) { + FiltersCacheController::putFiltered(item, res.value()); + return res.value(); + } + return false; + +} +} diff --git a/Telegram/SourceFiles/ayu/features/filters/filters_controller.h b/Telegram/SourceFiles/ayu/features/filters/filters_controller.h new file mode 100644 index 0000000000..099027f02f --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/filters/filters_controller.h @@ -0,0 +1,52 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once + +#include +#include +#include "unicode/regex.h" + + +using namespace icu_78; +namespace FiltersController { +bool isEnabled(PeerData* peer); +bool isBlocked(not_null item); +bool filteredWithoutCaching(not_null historyItem); +bool filtered(not_null historyItem); + + +struct ReversiblePattern +{ + std::shared_ptr pattern; + bool reversed; +}; + +struct HashablePattern +{ + std::vector id; + ReversiblePattern pattern; + + bool operator==(const HashablePattern &other) const { + return id == other.id; + // && pattern.pattern == other.pattern.pattern + // && pattern.reversed == other.pattern.reversed; + } +}; + +// im unable to override std::hash of HashablePattern (skill issue) so use hasher +struct PatternHasher +{ + std::size_t operator()(const HashablePattern &p) const { + std::string_view view(p.id.data(), p.id.size()); + return std::hash{}(view); + } +}; + + + +} + diff --git a/Telegram/SourceFiles/ayu/features/filters/filters_utils.cpp b/Telegram/SourceFiles/ayu/features/filters/filters_utils.cpp new file mode 100644 index 0000000000..5aea6bff2d --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/filters/filters_utils.cpp @@ -0,0 +1,509 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#include "filters_utils.h" + +#include +#include +#include +#include + +#include "ayu/data/ayu_database.h" +#include "ui/toast/toast.h" +#include +#include +#include + +#include "filters_cache_controller.h" +#include "ayu/ayu_settings.h" +#include "ayu/utils/telegram_helpers.h" +#include "data/data_document.h" +#include "data/data_session.h" +#include "history/history_item.h" +#include "main/main_account.h" +#include "main/main_domain.h" +#include "main/main_session.h" + + +constexpr auto BACKUP_VERSION = 2; + +void FilterUtils::importFromLink(const QString &link) { + if (link.isEmpty()) { + Ui::Toast::Show(tr::ayu_FiltersToastFailFetch(tr::now)); + return; + } + + const auto request = QNetworkRequest(QUrl(link)); + + + + _reply = _manager->get(request); + + connect(_reply, &QNetworkReply::finished, this, [=]{ + const QByteArray response = _reply->readAll(); + + if (!handleResponse(response)) { + LOG(("Filters import: Error handling response or bad map size: %1").arg(response.size())); + } + + _reply->deleteLater(); + }); + + connect(_reply, &QNetworkReply::errorOccurred, this, [=](QNetworkReply::NetworkError e) { + gotFailure(e); + + _reply->deleteLater(); + }); +} + +bool FilterUtils::importFromJson(const QByteArray &json) { + auto error = QJsonParseError{0, QJsonParseError::NoError}; + const auto document = QJsonDocument::fromJson(json, &error); + + if (error.error != QJsonParseError::NoError) { + LOG(("FilterUtils: Failed to parse JSON, error: %1" + ).arg(error.errorString())); + return false; + } + if (!document.isObject()) { + LOG(("FilterUtils: not an object received in JSON")); + return false; + } + const auto changes = prepareChanges(document.object()); + + if (changes == ApplyChanges{}) { + Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now)); + return false; + } + + const auto any = !changes.newFilters.empty() || !changes.removeFiltersById.empty() || !changes.filtersOverrides.empty() || !changes.newExclusions.empty() || !changes.removeExclusions.empty() || !changes.peersToBeResolved.empty(); + + if (!any) { + Ui::Toast::Show(tr::ayu_FiltersToastFailNoChanges(tr::now)); + return false; + } + + applyChanges(changes); + + return true; + +} + +struct BackupExclusion +{ + ID dialogId; + std::vector filterId; + + QJsonObject toJson() const { + QJsonObject json; + json["dialogId"] = dialogId; + json["filterId"] = QString::fromUtf8(filterId); + return json; + } +}; +QString FilterUtils::exportFilters() { + auto createJsonArray = [&](const auto &container) { + QJsonArray jsonArray; + for (const auto &item : container) { + jsonArray.append(item.toJson()); + } + return jsonArray; + }; + + + QJsonObject jsonObject; + jsonObject["version"] = BACKUP_VERSION; + const auto filters = AyuDatabase::getAllRegexFilters(); + jsonObject["filters"] = createJsonArray(filters); + + const auto excl = AyuDatabase::getAllFiltersExclusions(); + + + std::vector exclusions; + exclusions.reserve(excl.size()); + + for (const auto &item : excl) { + exclusions.push_back(BackupExclusion{item.dialogId, item.filterId}); + } + + jsonObject["exclusions"] = createJsonArray(exclusions); + + QJsonObject peers; + for (const auto &item : filters) { + const auto ¤tSession = Core::App().domain().active().session(); + if (!item.dialogId.has_value()) { + continue; + } + if (const auto peer = currentSession.data().peer(peerFromChat(abs(item.dialogId.value())))) { + if (!peer->username().isEmpty()) { + QString key = QString::number(item.dialogId.value()); + peers[key] = peer->username(); + } + } + + } + jsonObject["peers"] = peers; + + + QJsonDocument jsonDoc(jsonObject); + QByteArray jsonData = jsonDoc.toJson(QJsonDocument::Indented); + return QString::fromUtf8(jsonData); + +} + + +// for compatibility with Android version + +int typeOfMessage(const HistoryItem *item) { + if (item->isSponsored()) { + return 0; // TYPE_TEXT + } + + if (!item->isService()) { + if (const auto media = item->media()) { + if (const auto invoice = media->invoice(); invoice && invoice->isPaidMedia) { + return 29; // TYPE_PAID_MEDIA + } + if (media->giveawayStart()) { + return 26; // TYPE_GIVEAWAY + } + if (media->giveawayResults()) { + return 28; // TYPE_GIVEAWAY_RESULTS + } + if (const auto dice = dynamic_cast(media)) { + return 15; // TYPE_ANIMATED_STICKER + } + if (media->photo()) { + return 1; // TYPE_PHOTO + } + if (media->location()) { + return 4; // TYPE_GEO + } + if (media->sharedContact()) { + return 12; // TYPE_CONTACT + } + if (media->poll() || media->todolist()) { + return 17; // TYPE_POLL + } + if (media->storyId().valid()) { + if (media->storyMention()) { + return 24; // TYPE_STORY_MENTION + } + return 23; // TYPE_STORY + } + if (const auto document = media->document()) { + if (document->round()) { + return 5; // TYPE_ROUND_VIDEO + } + if (document->isVideoFile()) { + return 3; // TYPE_VIDEO + } + if (document->isVoiceMessage()) { + return 2; // TYPE_VOICE + } + if (document->isAudioFile()) { + return 14; // TYPE_MUSIC + } + if (document->isAnimation()) { + return 8; // TYPE_GIF + } + if (document->sticker()) { + if (document->isAnimation()) { + return 15; // TYPE_ANIMATED_STICKER + } + return 13; // TYPE_STICKER + } + return 9; // TYPE_FILE + } + if (media->game() || media->invoice() || media->webpage()) { + return 0; // TYPE_TEXT + } + } else { + if (item->isOnlyEmojiAndSpaces()) { + return 19; // TYPE_EMOJIS + } + return 0; // TYPE_TEXT + } + } else { + if (const auto media = item->media()) { + if (media->call()) { + return 16; // TYPE_PHONE_CALL + } + if (media->photo() && !item->isUserpicSuggestion()) { + return 11; // TYPE_ACTION_PHOTO + } + if (media->photo() && item->isUserpicSuggestion()) { + return 21; // TYPE_SUGGEST_PHOTO + } + if (media->paper()) { + return 22; // TYPE_ACTION_WALLPAPER + } + if (const auto gift = media->gift()) { + if (gift->type == Data::GiftType::Premium) { + if (gift->channel) { + return 25; // TYPE_GIFT_PREMIUM_CHANNEL + } + return 18; // TYPE_GIFT_PREMIUM + } + if (gift->type == Data::GiftType::Credits + || gift->type == Data::GiftType::StarGift + || gift->type == Data::GiftType::Ton) { + return 30; // TYPE_GIFT_STARS + } + } + if (item->Get()) { + return 28; // TYPE_GIVEAWAY_RESULTS + } + } + return 10; // TYPE_DATE + } + return 0; // TYPE_TEXT +} +QString FilterUtils::extractAllText(not_null item) { + QString text(item->originalText().text); + if (!item->originalText().entities.empty()) { + for (const auto &entity : item->originalText().entities) { + if (entity.type() == EntityType::Url || entity.type() == EntityType::CustomUrl) { + text.append("\n"); + text.append(entity.data()); + } + } + text.append("\n"); + } + + if (const auto markup = item->Get()) { + if (!markup->data.isNull()) { + for (const auto &row : markup->data.rows) { + for (const auto &button : row) { + text.append(""); + text.append("\n"); + } + } + } + } + + text.append("\n").append("").append(QString::number(typeOfMessage(item))).append(""); + + return text; +} + +bool FilterUtils::handleResponse(const QByteArray &response) { + try { + return importFromJson(response); + } catch (...) { + LOG(("FilterUtils: Failed to apply response")); + return false; + } +} + +void FilterUtils::gotFailure(const QNetworkReply::NetworkError &error) { + LOG(("FilterUtils: Error %1").arg(error)); + Ui::Toast::Show(tr::ayu_FiltersToastFailFetch(tr::now)); +} + +ApplyChanges FilterUtils::prepareChanges(const QJsonObject &root) { + const auto version = root.value("version"); + + if (version.toInt() > BACKUP_VERSION) { + return {}; + } + + + const auto existingFilters = AyuDatabase::getAllRegexFilters(); + const auto existingExclusions = AyuDatabase::getAllFiltersExclusions(); + + std::vector filtersOverrides; + std::map, RegexFilter> newFilters; + std::vector newExclusions; + std::vector removeFiltersById; + std::vector removeExclusions; + std::map peersToBeResolved; + + + if (const auto &filters = root.value("filters").toArray(); !filters.isEmpty()) { + for (const auto &filterRef : filters) { + if (const auto filter = filterRef.toObject(); !filter.isEmpty()) { + RegexFilter regex; + regex.caseInsensitive = filter.value("caseInsensitive").toBool(); + + const auto dialogIdValue = filter.value("dialogId"); + if (!dialogIdValue.isNull()) { + regex.dialogId = filter.value("dialogId").toInteger(); + } else { + regex.dialogId = std::nullopt; + } + regex.enabled = filter.value("enabled").toBool(); + + auto byteArray = filter.value("id").toString().toUtf8(); + regex.id = std::vector(byteArray.constData(), byteArray.constData() + byteArray.size()); + + regex.reversed = filter.value("reversed").toBool(); + regex.text = filter.value("text").toString().toStdString(); + + + auto it = std::ranges::find_if(existingFilters, + [®ex](const RegexFilter &f) + { + return f.id == regex.id; + }); + if (it != existingFilters.end()) { + const RegexFilter &existing = *it; + if (existing != regex) { + filtersOverrides.push_back(existing); + } + } else { + newFilters[regex.id] = std::move(regex); + } + } + } + } + + if (const auto exclusions = root.value("exclusions").toArray(); !exclusions.isEmpty()) { + for (const auto &exclusionRef : exclusions) { + if (const auto exclusion = exclusionRef.toObject(); !exclusion.isEmpty()) { + RegexFilterGlobalExclusion regex; + + regex.dialogId = exclusion.value("dialogId").toInteger(); + + auto byteArray = exclusion.value("filterId").toString().toUtf8(); + regex.filterId = std::vector(byteArray.constData(), byteArray.constData() + byteArray.size()); + + auto it = std::ranges::find_if(existingExclusions, + [®ex](const RegexFilterGlobalExclusion &f) + { + return f.dialogId == regex.dialogId && f.filterId == regex.filterId; + }); + + if (it == existingExclusions.end()) { + newExclusions.push_back(std::move(regex)); + } + } + } + } + + if (const auto removeFiltersByIdJson = root.value("removeFiltersById").toArray(); !removeFiltersByIdJson.isEmpty()) { + for (const auto &filterRef : removeFiltersByIdJson) { + const auto filter = filterRef.toString(); + + const auto byteArray = filter.toUtf8(); + const auto filterId = std::vector(byteArray.constData(), byteArray.constData() + byteArray.size()); + + const auto exists = std::ranges::any_of(existingFilters, + [&](const RegexFilter &f) { + return f.id == filterId; + }); + if (exists) { + removeFiltersById.push_back(filter); + } + + } + } + + if (const auto removeExclusionsJson = root.value("removeExclusions").toArray(); !removeExclusionsJson.isEmpty()) { + for (const auto &exclusionRef : removeExclusionsJson) { + const auto exclusionObj = exclusionRef.toObject(); + const auto filterIdStr = exclusionObj.value("filterId").toString(); + const qint64 dialogId = exclusionObj.value("dialogId").toVariant().toLongLong(); + + const auto byteArray = filterIdStr.toUtf8(); + const auto filterIdVec = std::vector(byteArray.constData(), byteArray.constData() + byteArray.size()); + + const bool exists = std::ranges::any_of(existingExclusions, + [&](const RegexFilterGlobalExclusion& x) { + return x.filterId == filterIdVec && x.dialogId == dialogId; + }); + + if (exists) { + RegexFilterGlobalExclusion regex; + regex.dialogId = dialogId; + regex.filterId = filterIdVec; + removeExclusions.push_back(regex); + } + } + } + + if (const auto peersJson = root.value("peers").toObject(); !peersJson.isEmpty()) { + for (const auto &dialogIdStr : peersJson.keys()) { + + bool parsed; + const auto dialogId = dialogIdStr.toLongLong(&parsed); + if (!parsed) { + continue; + } + + // almost everytime fails + PeerData* peerMaybe = nullptr; + for (const auto &[index, account] : Core::App().domain().accounts()) { + if (const auto session = account->maybeSession()) { + if (const auto peer = session->data().peer(peerFromChat(abs(dialogId)))) { + peerMaybe = peer; + break; + } + } + } + + if (!peerMaybe) { + const auto username = peersJson.value(dialogIdStr).toString(); + peersToBeResolved[dialogId] = username; + } + + } + } + + ApplyChanges changes; + for (auto &filter : newFilters) { + changes.newFilters.push_back(std::move(filter.second)); + } + changes.removeFiltersById = std::move(removeFiltersById); + changes.filtersOverrides = std::move(filtersOverrides); + changes.newExclusions = std::move(newExclusions); + changes.removeExclusions = std::move(removeExclusions); + changes.peersToBeResolved = std::move(peersToBeResolved); + + return changes; +} + +void FilterUtils::applyChanges(const ApplyChanges &changes) { + if (!changes.newFilters.empty()) { + for (const auto &filter : changes.newFilters) { + AyuDatabase::addRegexFilter(filter); + } + } + + if (!changes.removeFiltersById.empty()) { + for (const auto &id : changes.removeFiltersById) { + const auto byteArray = id.toUtf8(); + const auto filterId = std::vector(byteArray.constData(), byteArray.constData() + byteArray.size()); + AyuDatabase::deleteExclusionsByFilterId(filterId); + } + } + + if (!changes.filtersOverrides.empty()) { + for (const auto &filter : changes.filtersOverrides) { + AyuDatabase::updateRegexFilter(filter); + } + } + + if (!changes.newExclusions.empty()) { + for (const auto &exclusion : changes.newExclusions) { + AyuDatabase::addRegexExclusion(exclusion); + } + } + + if (!changes.removeExclusions.empty()) { + for (const auto &exclusion : changes.removeExclusions) { + AyuDatabase::deleteExclusion(exclusion.dialogId, exclusion.filterId); + } + } + + if (!changes.peersToBeResolved.empty()) { + resolveAllChats(changes.peersToBeResolved); + } + + FiltersCacheController::rebuildCache(); + +} + diff --git a/Telegram/SourceFiles/ayu/features/filters/filters_utils.h b/Telegram/SourceFiles/ayu/features/filters/filters_utils.h new file mode 100644 index 0000000000..d5dbdf5951 --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/filters/filters_utils.h @@ -0,0 +1,83 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once +#include +#include + +#include "ayu/data/entities.h" +#include "core/application.h" + +#include "api/api_bot.h" +#include "core/click_handler_types.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "history/history_item_components.h" +#include "main/main_session.h" +#include "ui/cached_round_corners.h" +#include "ui/painter.h" +#include "ui/ui_utility.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_widgets.h" + +struct ApplyChanges +{ + std::vector newFilters; + std::vector removeFiltersById; + + std::vector filtersOverrides; + + std::vector newExclusions; + std::vector removeExclusions; + + std::map peersToBeResolved; + + auto operator<=>(const ApplyChanges&) const = default; +}; + + +class FilterUtils final: public QObject +{ + + + Q_OBJECT +public: + static FilterUtils &getInstance() { + static FilterUtils instance; + return instance; + } + FilterUtils(const FilterUtils &) = delete; + FilterUtils &operator=(const FilterUtils &) = delete; + FilterUtils(FilterUtils &&) = delete; + FilterUtils &operator=(FilterUtils &&) = delete; + + + void importFromLink(const QString &link); + bool importFromJson(const QByteArray &json); + + static QString exportFilters(); + + static QString extractAllText(not_null item); +private: + FilterUtils(): + _manager(std::make_unique()) { + + } + + bool handleResponse(const QByteArray &response); + void gotFailure(const QNetworkReply::NetworkError &error); + + ApplyChanges prepareChanges(const QJsonObject &response); + void applyChanges(const ApplyChanges &changes); + + QTimer* _timer = nullptr; + + std::unique_ptr _manager = nullptr; + QNetworkReply *_reply = nullptr; +}; + diff --git a/Telegram/SourceFiles/ayu/features/filters/shadow_ban_utils.cpp b/Telegram/SourceFiles/ayu/features/filters/shadow_ban_utils.cpp new file mode 100644 index 0000000000..aef76a9f85 --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/filters/shadow_ban_utils.cpp @@ -0,0 +1,64 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#include "shadow_ban_utils.h" + +#include + +#include "filters_cache_controller.h" +#include "ayu/ayu_settings.h" +#include "ayu/data/entities.h" + +std::unordered_set ShadowBanUtils::shadowBanList; + +void ShadowBanUtils::reloadShadowBan() { + loadShadowBanList(); +} + + +void ShadowBanUtils::addShadowBan(ID userId) { + if (shadowBanList.insert(userId).second) { + setShadowBanList(); + } +} + +void ShadowBanUtils::removeShadowBan(ID userId) { + if (shadowBanList.erase(userId) > 0) { + setShadowBanList(); + } +} + +bool ShadowBanUtils::isShadowBanned(ID userId) { + return shadowBanList.contains(userId); +} + +void ShadowBanUtils::loadShadowBanList() { + auto &settings = AyuSettings::getInstance(); + + if (settings.shadowBanIds.isEmpty()) { + return; + } + const auto idList = settings.shadowBanIds.split(',', Qt::SkipEmptyParts); + + for (const auto &id : idList) { + shadowBanList.insert(id.toLongLong()); + } +} +const std::unordered_set &ShadowBanUtils::getShadowBanList() { + return shadowBanList; +} +void ShadowBanUtils::setShadowBanList() { + QStringList idStringList; + idStringList.reserve(shadowBanList.size()); + for (const auto &id : shadowBanList) { + idStringList.push_back(QString::number(id)); + } + + FiltersCacheController::rebuildCache(); + + AyuSettings::set_shadowBanIds(idStringList.join(",")); + AyuSettings::save(); +} \ No newline at end of file diff --git a/Telegram/SourceFiles/ayu/features/filters/shadow_ban_utils.h b/Telegram/SourceFiles/ayu/features/filters/shadow_ban_utils.h new file mode 100644 index 0000000000..7318aaa4b2 --- /dev/null +++ b/Telegram/SourceFiles/ayu/features/filters/shadow_ban_utils.h @@ -0,0 +1,24 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once +#include + +class ShadowBanUtils { +public: + static void reloadShadowBan(); + static void addShadowBan(long long userId); + static void removeShadowBan(long long userId); + static bool isShadowBanned(long long userId); + +private: + ShadowBanUtils() = delete; + + static const std::unordered_set &getShadowBanList(); + static void loadShadowBanList(); + static void setShadowBanList(); + static std::unordered_set shadowBanList; +}; diff --git a/Telegram/SourceFiles/ayu/ui/ayu_icons.style b/Telegram/SourceFiles/ayu/ui/ayu_icons.style index e8c2420041..0489cc31cc 100644 --- a/Telegram/SourceFiles/ayu/ui/ayu_icons.style +++ b/Telegram/SourceFiles/ayu/ui/ayu_icons.style @@ -10,6 +10,7 @@ using "ui/colors.palette"; using "ui/widgets/widgets.style"; using "dialogs/dialogs.style"; +using "info/info.style"; ayuGhostIcon: icon {{ "ayu/ghost", menuIconColor }}; ayuMenuIcon: icon {{ "ayu/ayu_menu", menuIconColor }}; @@ -52,3 +53,15 @@ dialogsExteraSupporterIcon: ThreeStateIcon { over: icon {{ "ayu/dialogs_extera_supporter", dialogsVerifiedIconBgOver }}; active: icon {{ "ayu/dialogs_extera_supporter", dialogsVerifiedIconBgActive }}; } +ayuFiltersAddIcon: IconButton(infoLayerTopBarClose) { + width: 40px; + icon: icon {{ "menu/add", boxTitleCloseFg }}; + iconOver: icon {{ "menu/add", boxTitleCloseFgOver }}; + iconPosition: point(8px, -1px); +} +ayuFiltersExcludeIcon: IconButton(infoLayerTopBarClose) { + width: 40px; + icon: icon {{ "ayu/streamer", boxTitleCloseFg }}; + iconOver: icon {{ "ayu/streamer", boxTitleCloseFgOver }}; + iconPosition: point(8px, -1px); +} \ No newline at end of file diff --git a/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.cpp b/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.cpp index 1d28a8297b..c515f9ff03 100644 --- a/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.cpp +++ b/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.cpp @@ -13,6 +13,7 @@ #include "ayu/ayu_settings.h" #include "ayu/ayu_state.h" #include "ayu/data/messages_storage.h" +#include "ayu/features/filters/shadow_ban_utils.h" #include "ayu/ui/context_menu/menu_item_subtext.h" #include "ayu/utils/qt_key_modifiers_extended.h" #include "history/history_item_components.h" @@ -210,6 +211,7 @@ void AddDeletedMessagesActions(PeerData *peerData, ->showSection(std::make_shared(peerData, nullptr, topicId)); }, &st::menuIconArchive); + // todo view filters } void AddJumpToBeginningAction(PeerData *peerData, @@ -313,6 +315,32 @@ void AddOpenChannelAction(PeerData *peerData, &st::menuIconChannel); } +void AddShadowBanAction(PeerData *peerData, + const Window::PeerMenuCallback &addCallback) { + const auto &settings = AyuSettings::getInstance(); + if (!peerData || !peerData->isUser() || !settings.filtersEnabled) { + return; + } + + const auto realId = peerData->id.value & PeerId::kChatTypeMask; + const auto toggleShadowBan = [=] + { + if (ShadowBanUtils::isShadowBanned(realId)) { + ShadowBanUtils::removeShadowBan(realId); + } else { + ShadowBanUtils::addShadowBan(realId); + } + }; + + addCallback({ + .text = (ShadowBanUtils::isShadowBanned(realId) + ? tr::ayu_FiltersQuickUnshadowBan(tr::now) + : tr::ayu_FiltersQuickShadowBan(tr::now)), + .handler = toggleShadowBan, + .icon = &st::menuIconStealth, +}); +} + void AddDeleteOwnMessagesAction(PeerData *peerData, Data::ForumTopic *topic, not_null sessionController, diff --git a/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.h b/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.h index 5fc9346c69..a5426a6a74 100644 --- a/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.h +++ b/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.h @@ -25,6 +25,8 @@ void AddJumpToBeginningAction(PeerData *peerData, not_null sessionController, const Window::PeerMenuCallback &addCallback); +void AddShadowBanAction(PeerData *peerData, + const Window::PeerMenuCallback &addCallback); void AddOpenChannelAction(PeerData *peerData, not_null sessionController, const Window::PeerMenuCallback &addCallback); diff --git a/Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.cpp b/Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.cpp new file mode 100644 index 0000000000..53f8d72159 --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.cpp @@ -0,0 +1,434 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#include "edit_filter.h" + +#include +#include +#include +#include +#include + +#include "ayu/ayu_settings.h" + +#include "lang_auto.h" + +#include "boxes/connection_box.h" +#include "styles/style_boxes.h" +#include "styles/style_settings.h" + +#include "ayu/data/ayu_database.h" +#include "ayu/features/filters/filters_cache_controller.h" +#include "ayu/utils/telegram_helpers.h" +#include "ui/qt_object_factory.h" +#include "ui/ui_utility.h" +#include "ui/vertical_list.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/fields/input_field.h" + +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" +#include "ui/widgets/fields/password_input.h" +#include "ui/widgets/labels.h" +#include "base/event_filter.h" +#include "ui/effects/animations.h" +#include "styles/style_window.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/text/text.h" +#include "media/view/media_view_overlay_widget.h" + +#include "apiwrap.h" +#include "api/api_attached_stickers.h" +#include "api/api_peer_photo.h" +#include "base/qt/qt_common_adapters.h" +#include "base/timer_rpl.h" +#include "lang/lang_keys.h" +#include "menu/menu_sponsored.h" +#include "boxes/premium_preview_box.h" +#include "core/application.h" +#include "core/click_handler_types.h" +#include "core/file_utilities.h" +#include "core/mime_type.h" +#include "core/ui_integration.h" +#include "core/crash_reports.h" +#include "core/sandbox.h" +#include "core/shortcuts.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/dropdown_menu.h" +#include "ui/widgets/popup_menu.h" +#include "ui/widgets/buttons.h" +#include "ui/layers/layer_manager.h" +#include "ui/text/text_utilities.h" +#include "ui/platform/ui_platform_window_title.h" +#include "ui/toast/toast.h" +#include "ui/text/format_values.h" +#include "ui/item_text_options.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/power_saving.h" +#include "ui/cached_round_corners.h" +#include "ui/gl/gl_window.h" +#include "ui/boxes/confirm_box.h" +#include "ui/ui_utility.h" +#include "info/info_memento.h" +#include "info/info_controller.h" +#include "info/statistics/info_statistics_widget.h" +#include "boxes/delete_messages_box.h" +#include "boxes/report_messages_box.h" +#include "media/audio/media_audio.h" +#include "media/view/media_view_group_thumbs.h" +#include "media/view/media_view_pip.h" +#include "media/view/media_view_overlay_raster.h" +#include "media/view/media_view_overlay_opengl.h" +#include "media/view/media_view_playback_sponsored.h" +#include "media/stories/media_stories_share.h" +#include "media/stories/media_stories_view.h" +#include "media/streaming/media_streaming_document.h" +#include "media/streaming/media_streaming_player.h" +#include "media/player/media_player_instance.h" +#include "history/history.h" +#include "history/history_item_helpers.h" +#include "history/view/media/history_view_media.h" +#include "history/view/reactions/history_view_reactions_selector.h" +#include "data/components/sponsored_messages.h" +#include "data/data_session.h" +#include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_user.h" +#include "data/data_media_rotation.h" +#include "data/data_photo_media.h" +#include "data/data_document_media.h" +#include "data/data_document_resolver.h" +#include "data/data_file_click_handler.h" +#include "data/data_download_manager.h" +#include "window/themes/window_theme_preview.h" +#include "window/window_peer_menu.h" +#include "window/window_controller.h" +#include "base/platform/base_platform_info.h" +#include "base/power_save_blocker.h" +#include "base/random.h" +#include "base/unixtime.h" +#include "base/qt_signal_producer.h" +#include "base/event_filter.h" +#include "main/main_account.h" +#include "main/main_domain.h" // Domain::activeSessionValue. +#include "main/main_session.h" +#include "main/main_session_settings.h" +#include "layout/layout_document_generic_preview.h" +#include "platform/platform_overlay_widget.h" +#include "storage/file_download.h" +#include "storage/storage_account.h" +#include "calls/calls_instance.h" +#include "styles/style_media_view.h" +#include "styles/style_calls.h" +#include "styles/style_chat.h" +#include "styles/style_menu_icons.h" + + +#include +#include +#include +#include +#include + +#include + +class PainterHighQualityEnabler; + +namespace Settings +{ + +std::vector generate_uuid_bytes() +{ + // stolen somewhere from Internet + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution dist; + + std::vector bytes(16); + for (int i = 0; i < 16; i += 4) { + uint32_t random_chunk = dist(gen); + bytes[i] = random_chunk & 0xFF; + bytes[i + 1] = (random_chunk >> 8) & 0xFF; + bytes[i + 2] = (random_chunk >> 16) & 0xFF; + bytes[i + 3] = (random_chunk >> 24) & 0xFF; + } + bytes[6] = (bytes[6] & 0x0F) | 0x40; + bytes[8] = (bytes[8] & 0x3F) | 0x80; + + return std::vector(bytes.begin(), bytes.end()); +} + +rpl::producer AyuEditFilters::title() +{ + return tr::ayu_RegexFiltersAdd(); +} + +object_ptr getCheckBox(not_null container, + const QString &label, + bool checked) +{ + return object_ptr( + container, + label, + checked, + st::settingsCheckbox); +} + +AyuEditFilters::AyuEditFilters( + QWidget *parent, + not_null controller) + : Section(parent) +{ + if (!controller->filterId.empty()) { + currentFilter = AyuDatabase::getById(controller->filterId); + } + setupContent(controller, parent); +} + +// unused currently, TODO: need to add this on bad regex pattern +not_null AddError( + not_null content, + Ui::PasswordInput *input) +{ + const auto error = content->add( + object_ptr>( + content, + object_ptr( + content, + // Set any text to resize. + tr::lng_language_name(tr::now), + st::settingLocalPasscodeError)), + st::changePhoneDescriptionPadding)->entity(); + error->hide(); + if (input) { + QObject::connect(input, + &Ui::MaskedInputField::changed, + [=] + { + error->hide(); + }); + } + return error; +}; + +void AyuEditFilters::setupSettings(not_null container, QWidget *parent) +{ + AddSkip(container); + + const auto add = [&](const QString &label, bool checked, auto &&handle) + { + auto check = container->add( + getCheckBox(container, label, checked), + st::settingsCheckboxPadding + ); + check->checkedChanges( + ) | rpl::start_with_next( + std::forward(handle), + container->lifetime()); + return check; + }; + + + const auto name = container->add( + object_ptr( + parent, + st::windowFilterNameInput, + Ui::InputField::Mode::MultiLine, + tr::ayu_RegexFiltersPlaceholder()), + st::markdownLinkFieldPadding); + + + auto enabled = add( + QString("Enable Filter"), + currentFilter.enabled, + [=, this](bool checked) + { + currentFilter.enabled = checked; + }); + + auto insensetive = add( + QString("Case Insensitive"), + currentFilter.caseInsensitive, + [=, this](bool checked) + { + currentFilter.caseInsensitive = checked; + }); + + auto reversed = add( + QString("Reversed"), + currentFilter.reversed, + [=, this](bool checked) + { + currentFilter.reversed = checked; + }); + + name->setText(QString::fromStdString(currentFilter.text)); + name->submits( + ) | rpl::start_with_next([=, this] + { + currentFilter.text = name->getTextWithTags().text.toStdString(); + + currentFilter.enabled = enabled->checked(); + currentFilter.caseInsensitive = insensetive->checked(); + currentFilter.reversed = reversed->checked(); + + if (currentFilter.id.empty()) { + currentFilter.id = generate_uuid_bytes(); + } + AyuDatabase::addRegexFilter(currentFilter); + + FiltersCacheController::rebuildCache(); + }, + name->lifetime()); +} + +void RegexEditBuilder( + not_null box, + RegexFilter *filter, + const Fn &onDone, + std::optional dialogId, + bool showToast +) +{ + RegexFilter data; + + if (filter) { + box->setTitle(tr::ayu_RegexFiltersEdit()); + data = *filter; + } else { + box->setTitle(tr::ayu_RegexFiltersAdd()); + data.reversed = false; + } + + const auto name = box->addRow( + object_ptr( + box->verticalLayout(), + st::windowFilterNameInput, + Ui::InputField::Mode::MultiLine, + tr::ayu_RegexFiltersPlaceholder()), + st::markdownLinkFieldPadding); + const auto enabled = box->addRow( + object_ptr( + box, + tr::ayu_EnableExpression(tr::now), + data.enabled, + st::defaultBoxCheckbox), + st::settingsCheckboxPadding); + const auto caseInsensitive = box->addRow( + object_ptr( + box, + tr::ayu_CaseInsensitiveExpression(tr::now), + data.caseInsensitive, + st::defaultBoxCheckbox), + st::settingsCheckboxPadding); + const auto reversed = box->addRow( + object_ptr( + box, + tr::ayu_ReversedExpression(tr::now), + data.reversed, + st::defaultBoxCheckbox), + st::settingsCheckboxPadding); + + name->setText(QString::fromStdString(data.text)); + + auto saveAndClose = [=, id = data.id] + { + RegexFilter newFilter; + newFilter.text = name->getTextWithTags().text.toStdString(); + newFilter.enabled = enabled->checked(); + newFilter.caseInsensitive = caseInsensitive->checked(); + newFilter.reversed = reversed->checked(); + + if (!showToast && dialogId.has_value()) { + newFilter.dialogId = dialogId; + } + + if (!id.empty()) { + newFilter.id = id; + } else { + newFilter.id = generate_uuid_bytes(); + } + + box->closeBox(); + + crl::async([=] + { + AyuDatabase::addRegexFilter(newFilter); + FiltersCacheController::rebuildCache(); + + crl::on_main([=] + { + if (onDone) { + onDone(newFilter); + } + AyuSettings::fire_filtersUpdate(); + + if (showToast) { + const auto onClick = [=](const auto &...) mutable{ + newFilter.dialogId = dialogId; + + AyuDatabase::updateRegexFilter(newFilter); + FiltersCacheController::rebuildCache(); + AyuSettings::fire_filtersUpdate(); + + return true; + }; + Ui::Toast::Show(Ui::Toast::Config{ + // .text = tr::ayu_RegexFilterBulletinText( + // tr::now, + // lt_link, + // Ui::Text::Link( + // Ui::Text::Bold( + // tr::ayu_RegexFilterBulletinAction(tr::now))), + // Ui::Text::RichLangValue), + + // TODO: reconsider + .text = tr::ayu_RegexFilterBulletinText( + tr::now + //, +// lt_link, +// Ui::Text::Link( +// Ui::Text::Bold( +// tr::ayu_RegexFilterBulletinAction(tr::now))), +// Ui::Text::WithEntities + ), + .filter = onClick, + .adaptive = true + }); + } + }); + }); + }; + + name->submits() | rpl::start_with_next(saveAndClose, name->lifetime()); + box->addButton(tr::lng_settings_save(), saveAndClose); + box->addButton(tr::lng_cancel(), [=] + { box->closeBox(); }); +} + +object_ptr RegexEditBox(RegexFilter *filter, + const Fn &onDone, + std::optional dialogId, + bool showToast) +{ + return Box(RegexEditBuilder, filter, onDone, dialogId, showToast); +} + +void AyuEditFilters::setupContent(not_null controller, QWidget *parent) +{ + const auto content = Ui::CreateChild(this); + + setupSettings(content, parent); + + ResizeFitChild(this, content); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.h b/Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.h new file mode 100644 index 0000000000..8d4e2ae012 --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.h @@ -0,0 +1,43 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once + +#include "ayu/data/entities.h" +#include "settings/settings_common_session.h" + +#include "ui/wrap/vertical_layout.h" +#include "chat_helpers/emoji_suggestions_widget.h" +#include "boxes/premium_limits_box.h" +#include "info/profile/info_profile_values.h" +#include "window/window_session_controller.h" +#include "base/unixtime.h" + +class BoxContent; + +namespace Window { +class Controller; +class SessionController; +} // namespace Window + +namespace Settings { + +class AyuEditFilters : public Section +{ +public: + AyuEditFilters(QWidget *parent, not_null controller); + void setupSettings(not_null container, QWidget *parent); + + [[nodiscard]] rpl::producer title() override; + +private: + void setupContent(not_null controller, QWidget *parent); + + RegexFilter currentFilter; +}; + +object_ptr RegexEditBox(RegexFilter* filter, const Fn &onDone, std::optional dialogId = std::nullopt, bool showToast = false); +} // namespace Settings diff --git a/Telegram/SourceFiles/ayu/ui/settings/filters/peer_global_exclusion.cpp b/Telegram/SourceFiles/ayu/ui/settings/filters/peer_global_exclusion.cpp new file mode 100644 index 0000000000..a37e0fec01 --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/settings/filters/peer_global_exclusion.cpp @@ -0,0 +1,169 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#include "peer_global_exclusion.h" + +#include "data/data_peer.h" +#include "main/main_session.h" +#include "styles/style_boxes.h" +#include "ui/painter.h" + +#include + +#include + +#include "settings_filters_list.h" +#include "ayu/data/ayu_database.h" +#include "ayu/utils/telegram_helpers.h" +#include "data/data_session.h" +#include "window/window_session_controller.h" + +namespace Settings { + +GlobalExclusionListRow::GlobalExclusionListRow(PeerId peer) + : PeerListRow(peer.value) +, peerId(peer) +{ + +} + +QString GlobalExclusionListRow::generateName() { + if (const auto peerCached = currentSession()->data().peerLoaded(peerId)) { + this->setPeer(peerCached); + return PeerListRow::generateName(); + } + return QString("UNKNOWN (ID: %1)").arg(QString::number(peerId.value & PeerId::kChatTypeMask)); + +} + +PaintRoundImageCallback GlobalExclusionListRow::generatePaintUserpicCallback(bool forceRound) { + if (const auto peerCached = currentSession()->data().peerLoaded(peerId)) { + this->setPeer(peerCached); + return PeerListRow::generatePaintUserpicCallback(forceRound); + } + + + + return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { + using namespace Ui; + const auto realId = peerId.value & PeerId::kChatTypeMask; + auto _userpicEmpty = std::make_unique( + EmptyUserpic::UserpicColor(realId % 7), + QString("U")); // U - Unknown + _userpicEmpty->paintCircle(p, x, y, outerWidth, size); + }; +} + +GlobalExclusionListController::GlobalExclusionListController(not_null session, + not_null controller) +: _session(session) +, _controller(controller) +{ + +} + +Main::Session &GlobalExclusionListController::session() const { + return *_session; +} + + +void GlobalExclusionListController::prepare() { + + const auto filters = AyuDatabase::getAllRegexFilters(); + const auto exclusions = AyuDatabase::getAllFiltersExclusions(); + + + if (exclusions.empty()) { + auto description = object_ptr( + nullptr, + tr::ayu_RegexFiltersListEmpty(tr::now), + computeListSt().about); + delegate()->peerListSetDescription(std::move(description)); + return; + } + + struct FilterCounts { + int filters = 0; + int exclusions = 0; + }; + std::unordered_map countsByDialogIds; + + + + for (const auto &filter : filters) { + if (filter.dialogId.has_value()) { + countsByDialogIds[filter.dialogId.value()].filters++; + } + } + for (const auto &exclusion : exclusions) { + countsByDialogIds[exclusion.dialogId].exclusions++; + } + + + + for (const auto &[id, count] : countsByDialogIds) { + PeerId peerId = PeerId(PeerIdHelper(id)); + + auto row = std::make_unique (peerId); + row->setCustomStatus(QString("%1 filters, %2 excluded").arg(count.filters).arg(count.exclusions), false); + + delegate()->peerListAppendRow(reinterpret_cast &&>(row)); + + } + + // sortByName(); + + delegate()->peerListRefreshRows(); +} + +void GlobalExclusionListController::rowClicked(not_null peer) { + _controller->dialogId = peer->id() & PeerId::kChatTypeMask; + _controller->showExclude = true; + _controller->showSettings(AyuFiltersList::Id()); +} + +//////////////////////////////////// + +SelectChatBoxController::SelectChatBoxController( + not_null controller, + Fn)> onSelectedCallback) + : ChatsListBoxController(&controller->session()) + , _controller(controller) + , _onSelectedCallback(std::move(onSelectedCallback)) { +} + +Main::Session &SelectChatBoxController::session() const { + return _controller->session(); +} + +void SelectChatBoxController::rowClicked(not_null row) { + if (_onSelectedCallback) { + _onSelectedCallback(row->peer()); + } +} + +std::unique_ptr SelectChatBoxController::createRow(not_null history) { + const auto peer = history->peer; + + const auto skip = + peer->isUser() || + //peer->forum() || + peer->monoforum() + ; + + if (skip) { + return nullptr; + } + auto result = std::make_unique( + history, + nullptr); + return result; +} + +void SelectChatBoxController::prepareViewHook() { + delegate()->peerListSetTitle(tr::ayu_FiltersMenuSelectChat()); +} +} diff --git a/Telegram/SourceFiles/ayu/ui/settings/filters/peer_global_exclusion.h b/Telegram/SourceFiles/ayu/ui/settings/filters/peer_global_exclusion.h new file mode 100644 index 0000000000..8b8efbbaf1 --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/settings/filters/peer_global_exclusion.h @@ -0,0 +1,71 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once + +#include "boxes/peer_list_box.h" +#include "boxes/peer_list_controllers.h" +#include "data/data_peer.h" +#include "history/history.h" + +class RegexFilterGlobalExclusion; + +namespace Main { +class Session; +} // namespace Main + +namespace Settings { + +class GlobalExclusionListRow final : public PeerListRow +{ +public: + explicit GlobalExclusionListRow(PeerId peer); + QString generateName() override; + PaintRoundImageCallback generatePaintUserpicCallback(bool forceRound) override; + +private: + PeerId peerId; +}; + +class GlobalExclusionListController final : public PeerListController +{ +public: + explicit GlobalExclusionListController(not_null session, + not_null controller); + + [[nodiscard]] Main::Session &session() const override; + + void prepare() override; + + void rowClicked(not_null row) override; + +private: + const not_null _session; + not_null _controller; +}; + +class SelectChatBoxController + : public ChatsListBoxController + , public base::has_weak_ptr +{ +public: + explicit SelectChatBoxController( + not_null controller, + Fn)> onSelectedCallback + ); + + Main::Session &session() const override; + void rowClicked(not_null row) override; + +protected: + std::unique_ptr createRow(not_null history) override; + void prepareViewHook() override; + +private: + not_null _controller; + Fn)> _onSelectedCallback; +}; +} // namespace Settings diff --git a/Telegram/SourceFiles/ayu/ui/settings/filters/settings_filters_list.cpp b/Telegram/SourceFiles/ayu/ui/settings/filters/settings_filters_list.cpp new file mode 100644 index 0000000000..67e0ce70ed --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/settings/filters/settings_filters_list.cpp @@ -0,0 +1,276 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#include "settings_filters_list.h" + +#include +#include + +#include "edit_filter.h" +#include "ayu/ayu_settings.h" + +#include "lang_auto.h" + +#include "boxes/connection_box.h" +#include "settings/settings_common.h" +#include "storage/localstorage.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" +#include "styles/style_widgets.h" + +#include "../../components/icon_picker.h" +#include "ayu/data/ayu_database.h" +#include "ayu/features/filters/filters_cache_controller.h" +#include "ayu/features/filters/filters_utils.h" +#include "rpl/mappers.h" +#include "ui/qt_object_factory.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/popup_menu.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" + +namespace Settings +{ + +rpl::producer AyuFiltersList::title() +{ + return tr::ayu_RegexFilters(); +} + +AyuFiltersList::AyuFiltersList( + QWidget *parent, + not_null controller) + : Section(parent), _controller(controller), _content(Ui::CreateChild(this)) +{ + + + if (_controller->dialogId.has_value()) { + dialogId = _controller->dialogId.value(); + } + + + setupContent(controller); +} + +void AyuFiltersList::addNewFilter(const RegexFilter &filter, bool exclusion) +{ + + // stolen from EditPrivacyBox + auto state = lifetime().make_state(filter); + auto buttonText = lifetime().make_state>( + QString::fromStdString(state->text)); + auto isRemoved = lifetime().make_state>(false); + + auto wrap = _content->add( + object_ptr>( + _content, + object_ptr