feat: message filters initial

This commit is contained in:
bleizix
2025-09-14 23:02:40 +05:00
committed by AlexeyZavar
parent 5b7926c590
commit 698e4a0af4
51 changed files with 3070 additions and 90 deletions
+3
View File
@@ -91,3 +91,6 @@
[submodule "Telegram/ThirdParty/xdg-desktop-portal"]
path = Telegram/ThirdParty/xdg-desktop-portal
url = https://github.com/flatpak/xdg-desktop-portal.git
[submodule "Telegram/lib_icu"]
path = Telegram/lib_icu
url = https://github.com/AyuGram/lib_icu.git
+26
View File
@@ -20,6 +20,10 @@ add_subdirectory(lib_webrtc)
add_subdirectory(lib_webview)
add_subdirectory(codegen)
# AyuGram filters
add_subdirectory(lib_icu)
# AyuGram filters
get_filename_component(src_loc SourceFiles REALPATH)
get_filename_component(res_loc Resources REALPATH)
@@ -92,6 +96,9 @@ PRIVATE
desktop-app::external_openssl
desktop-app::external_openal
desktop-app::external_xxhash
# AyuGram filters
ayugram::lib_icu
)
set(ayugram_files
@@ -137,10 +144,18 @@ set(ayugram_files
ayu/ui/settings/settings_general.h
ayu/ui/settings/settings_ayu.cpp
ayu/ui/settings/settings_ayu.h
ayu/ui/settings/settings_filters.cpp
ayu/ui/settings/settings_filters.h
ayu/ui/settings/settings_main.cpp
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/per_dialog_filter.cpp
ayu/ui/settings/filters/per_dialog_filter.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
@@ -155,6 +170,8 @@ set(ayugram_files
ayu/ui/boxes/edit_mark_box.h
ayu/ui/boxes/font_selector.cpp
ayu/ui/boxes/font_selector.h
ayu/ui/boxes/import_filters_box.cpp
ayu/ui/boxes/import_filters_box.h
ayu/ui/boxes/theme_selector_box.cpp
ayu/ui/boxes/theme_selector_box.h
ayu/ui/boxes/message_shot_box.cpp
@@ -198,6 +215,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
@@ -2036,6 +2061,7 @@ PRIVATE
TDESKTOP_API_ID=${TDESKTOP_API_ID}
TDESKTOP_API_HASH=${TDESKTOP_API_HASH}
G_LOG_DOMAIN="Telegram"
U_STATIC_IMPLEMENTATION=1 # AyuGram: do not use __declspec(dllimport) when including icu headers
)
get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+8 -8
View File
@@ -6987,9 +6987,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_ImportDataSuccess" = "Database imported successfully.";
"ayu_ImportDataFailure" = "Failed to import database.";
"ayu_RegexFilters" = "Message Filters";
"ayu_RegexFiltersAmount#one" = "1 filter";
"ayu_RegexFiltersAmount#one" = "{count} filter";
"ayu_RegexFiltersAmount#other" = "{count} filters";
"ayu_RegexFiltersExcludedAmount#one" = "1 excluded";
"ayu_RegexFiltersExcludedAmount#one" = "{count} excluded";
"ayu_RegexFiltersExcludedAmount#other" = "{count} excluded";
"ayu_RegexFiltersHeader" = "Filters";
"ayu_RegexFiltersShared" = "Shared Filters";
@@ -7029,17 +7029,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_FiltersToastFailNoChanges" = "Filters are the same as current.";
"ayu_FiltersToastSuccess" = "Filters successfully imported.";
"ayu_FiltersSheetTitle" = "Apply filter changes?";
"ayu_FiltersSheetNewFilters#one" = "**1** new filter";
"ayu_FiltersSheetNewFilters#one" = "**{count}** new filter";
"ayu_FiltersSheetNewFilters#other" = "**{count}** new filters";
"ayu_FiltersSheetRemovedFilters#one" = "**1** removed filter";
"ayu_FiltersSheetRemovedFilters#one" = "**{count}** removed filter";
"ayu_FiltersSheetRemovedFilters#other" = "**{count}** removed filters";
"ayu_FiltersSheetUpdatedFilters#one" = "**1** updated filter";
"ayu_FiltersSheetUpdatedFilters#one" = "**{count}** updated filter";
"ayu_FiltersSheetUpdatedFilters#other" = "**{count}** updated filters";
"ayu_FiltersSheetNewExclusions#one" = "**1** new exclusion";
"ayu_FiltersSheetNewExclusions#one" = "**{count}** new exclusion";
"ayu_FiltersSheetNewExclusions#other" = "**{count}** new exclusions";
"ayu_FiltersSheetRemovedExclusions#one" = "**1** removed exclusion";
"ayu_FiltersSheetRemovedExclusions#one" = "**{count}** removed exclusion";
"ayu_FiltersSheetRemovedExclusions#other" = "**{count}** removed exclusions";
"ayu_FiltersSheetDialogsToResolve#one" = "**1** dialog to resolve";
"ayu_FiltersSheetDialogsToResolve#one" = "**{count}** dialog to resolve";
"ayu_FiltersSheetDialogsToResolve#other" = "**{count}** dialogs to resolve";
"ayu_FiltersClearPopupTitle" = "Clear filters";
"ayu_FiltersClearPopupText" = "Are you sure you want to clear all filters?";
+1
View File
@@ -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"
+24 -7
View File
@@ -44,7 +44,8 @@ rpl::variable<int> showPeerIdReactive;
rpl::variable<QString> translationProviderReactive;
rpl::variable<bool> hideFromBlockedReactive;
rpl::event_stream<> filtersUpdateReactive; // triggered on adding / editing filter
rpl::event_stream<> historyUpdateReactive;
rpl::lifetime lifetime = rpl::lifetime();
@@ -142,8 +143,6 @@ void postinitialize() {
showPeerIdReactive = settings->showPeerId;
translationProviderReactive = settings->translationProvider;
hideFromBlockedReactive = settings->hideFromBlocked;
ghostModeEnabled = ghostModeEnabled_util(settings.value());
}
@@ -226,6 +225,8 @@ AyuGramSettings::AyuGramSettings() {
saveForBots = false;
// ~ Message filters
filtersEnabled = false;
filtersEnabledInChats = false;
hideFromBlocked = false;
// ~ QoL toggles
@@ -257,7 +258,7 @@ AyuGramSettings::AyuGramSettings() {
#else
AyuAssets::DEFAULT_ICON
#endif
;
;
simpleQuotesAndReplies = true;
hideFastShare = false;
replaceBottomInfoWithIcons = true;
@@ -274,6 +275,7 @@ AyuGramSettings::AyuGramSettings() {
showHideMessageInContextMenu = 0;
showUserMessagesInContextMenu = 2;
showMessageDetailsInContextMenu = 2;
showAddFilterInContextMenu = 1;
showAttachButtonInMessageField = true;
showCommandsButtonInMessageField = true;
@@ -397,9 +399,16 @@ void set_saveForBots(bool val) {
settings->saveForBots = val;
}
void set_filtersEnabled(bool val) {
settings->filtersEnabled = val;
}
void set_filtersEnabledInChats(bool val) {
settings->filtersEnabledInChats = val;
}
void set_hideFromBlocked(bool val) {
settings->hideFromBlocked = val;
hideFromBlockedReactive = val;
}
void set_disableAds(bool val) {
@@ -516,6 +525,10 @@ void set_showMessageDetailsInContextMenu(int val) {
settings->showMessageDetailsInContextMenu = val;
}
void set_showAddFilterInContextMenu(int val) {
settings->showAddFilterInContextMenu = val;
}
void set_showAttachButtonInMessageField(bool val) {
settings->showAttachButtonInMessageField = val;
triggerHistoryUpdate();
@@ -694,8 +707,12 @@ rpl::producer<bool> get_ghostModeEnabledReactive() {
return ghostModeEnabled.value();
}
rpl::producer<bool> get_hideFromBlockedReactive() {
return hideFromBlockedReactive.value();
void fire_filtersUpdate() {
filtersUpdateReactive.fire({});
}
rpl::producer<> get_filtersUpdate() {
return filtersUpdateReactive.events();
}
void triggerHistoryUpdate() {
+18 -1
View File
@@ -6,6 +6,7 @@
// Copyright @Radolyn, 2025
#pragma once
#include <unordered_set>
#include "ayu/libs/json.hpp"
#include "ayu/libs/json_ext.hpp"
@@ -59,6 +60,9 @@ public:
bool saveForBots;
std::unordered_set<long long> shadowBanIds;
bool filtersEnabled;
bool filtersEnabledInChats;
bool hideFromBlocked;
bool disableAds;
@@ -95,6 +99,7 @@ public:
int showHideMessageInContextMenu;
int showUserMessagesInContextMenu;
int showMessageDetailsInContextMenu;
int showAddFilterInContextMenu;
bool showAttachButtonInMessageField;
bool showCommandsButtonInMessageField;
@@ -160,6 +165,8 @@ void set_saveMessagesHistory(bool val);
void set_saveForBots(bool val);
void set_filtersEnabled(bool val);
void set_filtersEnabledInChats(bool val);
void set_hideFromBlocked(bool val);
void set_disableAds(bool val);
@@ -196,6 +203,7 @@ void set_showViewsPanelInContextMenu(int val);
void set_showHideMessageInContextMenu(int val);
void set_showUserMessagesInContextMenu(int val);
void set_showMessageDetailsInContextMenu(int val);
void set_showAddFilterInContextMenu(int val);
void set_showAttachButtonInMessageField(bool val);
void set_showCommandsButtonInMessageField(bool val);
@@ -255,6 +263,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)
@@ -282,6 +293,7 @@ inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nloh
NLOHMANN_JSON_TO(showHideMessageInContextMenu)
NLOHMANN_JSON_TO(showUserMessagesInContextMenu)
NLOHMANN_JSON_TO(showMessageDetailsInContextMenu)
NLOHMANN_JSON_TO(showAddFilterInContextMenu)
NLOHMANN_JSON_TO(showAttachButtonInMessageField)
NLOHMANN_JSON_TO(showCommandsButtonInMessageField)
NLOHMANN_JSON_TO(showEmojiButtonInMessageField)
@@ -334,6 +346,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)
@@ -361,6 +376,7 @@ inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nl
NLOHMANN_JSON_FROM_WITH_DEFAULT(showHideMessageInContextMenu)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showUserMessagesInContextMenu)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageDetailsInContextMenu)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showAddFilterInContextMenu)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showAttachButtonInMessageField)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showCommandsButtonInMessageField)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showEmojiButtonInMessageField)
@@ -418,7 +434,8 @@ bool isUseScheduledMessages();
rpl::producer<bool> get_ghostModeEnabledReactive();
rpl::producer<bool> get_hideFromBlockedReactive();
void fire_filtersUpdate();
rpl::producer<> get_filtersUpdate();
void triggerHistoryUpdate();
rpl::producer<> get_historyUpdateReactive();
+191 -1
View File
@@ -111,7 +111,7 @@ auto storage = make_storage(
),
make_table<RegexFilter>(
"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,195 @@ bool hasDeletedMessages(ID userId, ID dialogId, ID topicId) {
return false;
}
}
template <typename T>
std::vector<T> getAllT() {
try {
return storage.get_all<T>();
} catch (std::exception &ex) {
LOG(("Failed to get all: %1").arg(ex.what()));
return {};
}
}
std::vector<RegexFilter> getAllRegexFilters() {
return getAllT<RegexFilter>();
}
std::vector<RegexFilterGlobalExclusion> getAllFiltersExclusions() {
return getAllT<RegexFilterGlobalExclusion>();
}
std::vector<RegexFilter> getExcludedByDialogId(ID dialogId) {
try {
return storage.get_all<RegexFilter>(
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<RegexFilter>();
} catch (std::exception &ex) {
LOG(("Failed to get count: %1").arg(ex.what()));
return 0;
}
}
RegexFilter getById(std::vector<char> id) {
try {
return storage.get<RegexFilter>(
where(column<RegexFilter>(&RegexFilter::id) == std::move(id))
);
} catch (std::exception &ex) {
LOG(("Failed to get filters by id: %1").arg(ex.what()));
return {};
}
}
std::vector<RegexFilter> getShared() {
try {
return storage.get_all<RegexFilter>(
where(is_null(column<RegexFilter>(&RegexFilter::dialogId)))
);
} catch (std::exception &ex) {
LOG(("Failed to get shared filters: %1").arg(ex.what()));
return {};
}
}
std::vector<RegexFilter> getByDialogId(ID dialogId) {
try {
return storage.get_all<RegexFilter>(
where(column<RegexFilter>(&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<char> 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<char> &id) {
try {
storage.remove_all<RegexFilter>(
where(column<RegexFilter>(&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<char> &id) {
try {
storage.remove_all<RegexFilterGlobalExclusion>(
where(column<RegexFilterGlobalExclusion>(&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<char> filterId) {
try {
storage.remove_all<RegexFilterGlobalExclusion>(
where(column<RegexFilterGlobalExclusion>(&RegexFilterGlobalExclusion::filterId) == filterId and
column<RegexFilterGlobalExclusion>(&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<RegexFilter>();
} catch (std::exception &ex) {
LOG(("Failed to delete all regex filter for some reason: %1").arg(ex.what()));
}
}
void deleteAllExclusions() {
try {
storage.remove_all<RegexFilterGlobalExclusion>();
} catch (std::exception &ex) {
LOG(("Failed to delete all regex filter exclusions for some reason: %1").arg(ex.what()));
}
}
bool hasFilters() {
try {
return !storage.select(
columns(column<RegexFilter>(&RegexFilter::id)),
limit(1)
).empty();
} catch (std::exception &ex) {
LOG(("Failed to check if there's any filters: %1").arg(ex.what()));
return false;
}
}
bool hasPerDialogFilters() {
try {
return
!storage.select(
columns(column<RegexFilter>(&RegexFilter::id)),
where(is_not_null(column<RegexFilter>(&RegexFilter::dialogId))),
limit(1)
).empty() ||
!storage.select(
columns(column<RegexFilterGlobalExclusion>(&RegexFilterGlobalExclusion::fakeId)),
limit(1)
).empty();
} catch (std::exception &ex) {
LOG(("Failed to check if there's any filters: %1").arg(ex.what()));
return false;
}
}
}
@@ -20,4 +20,29 @@ void addDeletedMessage(const DeletedMessage &message);
std::vector<DeletedMessage> getDeletedMessages(ID userId, ID dialogId, ID topicId, ID minId, ID maxId, int totalLimit);
bool hasDeletedMessages(ID userId, ID dialogId, ID topicId);
std::vector<RegexFilter> getAllRegexFilters();
RegexFilter getById(std::vector<char> id);
std::vector<RegexFilter> getShared();
std::vector<RegexFilter> getByDialogId(ID dialogId);
std::vector<RegexFilterGlobalExclusion> getAllFiltersExclusions();
std::vector<RegexFilter> 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<char> &id);
void deleteExclusionsByFilterId(const std::vector<char> &id);
void deleteExclusion(ID dialogId, std::vector<char> filterId);
void deleteAllFilters();
void deleteAllExclusions();
bool hasFilters();
bool hasPerDialogFilters();
}
+26 -1
View File
@@ -80,7 +80,28 @@ public:
bool enabled;
bool reversed;
bool caseInsensitive;
std::unique_ptr<ID> dialogId; // nullable
std::optional<ID> 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.data());
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<char> filterId;
bool operator==(const RegexFilterGlobalExclusion& other) const {
return dialogId == other.dialogId && filterId == other.filterId;
}
};
class SpyMessageRead
@@ -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 <unordered_set>
#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<std::vector<HashablePattern>> sharedPatterns;
std::optional<std::unordered_map<long long, std::vector<ReversiblePattern>>> patternsByDialogId;
std::optional<std::unordered_map<long long, std::unordered_set<HashablePattern, PatternHasher>>> exclusionsByDialogId;
std::unordered_map<long long, std::unordered_map<std::optional<int>,std::optional<bool>>> filteredMessages;
void rebuildCache() {
std::lock_guard lock(mutex);
const auto filters = AyuDatabase::getAllRegexFilters();
const auto exclusions = AyuDatabase::getAllFiltersExclusions();
std::vector<HashablePattern> shared;
std::unordered_map<long long, std::vector<ReversiblePattern>> 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<RegexPattern>(pattern), filter.reversed});
} else {
shared.push_back({filter.id, {std::shared_ptr<RegexPattern>(pattern), filter.reversed}});
}
}
auto exclByDialogId = buildExclusions(exclusions, shared);
sharedPatterns = shared;
patternsByDialogId = byDialogId;
exclusionsByDialogId = exclByDialogId;
filteredMessages.clear();
}
std::unordered_map<long long, std::unordered_set<HashablePattern, PatternHasher>> buildExclusions(
const std::vector<RegexFilterGlobalExclusion> &exclusions,
const std::vector<HashablePattern> &shared) {
std::unordered_map<long long, std::unordered_set<HashablePattern, PatternHasher>> 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<bool> isFiltered(not_null<HistoryItem *> 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<HistoryItem *> item, bool res) {
std::lock_guard lock(mutex);
filteredMessages[item->history()->peer->id.value][item->id.bare] = res;
}
std::optional<std::vector<ReversiblePattern>> 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<const std::unordered_set<HashablePattern, PatternHasher>> 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<HashablePattern> &getSharedPatterns() {
if (!sharedPatterns.has_value()) {
rebuildCache();
}
return sharedPatterns.value();
}
}
@@ -0,0 +1,31 @@
// 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 "filters_controller.h"
#include "ayu/data/entities.h"
using namespace FiltersController;
namespace FiltersCacheController {
void rebuildCache();
std::unordered_map<long long, std::unordered_set<HashablePattern, PatternHasher>> buildExclusions(
const std::vector<RegexFilterGlobalExclusion>& exclusions,
const std::vector<HashablePattern>& shared);
std::optional<bool> isFiltered(not_null<HistoryItem *> item);
void putFiltered(not_null<HistoryItem *> item, bool res);
std::optional<std::vector<ReversiblePattern>> getPatternsByDialogId(uint64 dialogId);
std::optional<const std::unordered_set<HashablePattern, PatternHasher>> getExclusionsByDialogId(long long dialogId);
const std::vector<HashablePattern> &getSharedPatterns();
}
@@ -0,0 +1,153 @@
// 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 <QTimer>
#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"
#include "ayu/utils/telegram_helpers.h"
namespace FiltersController {
bool filterBlocked(const not_null<HistoryItem*> item) {
if (item->from() != item->history()->peer) {
if (isBlocked(item)) {
return true;
}
}
return false;
}
std::optional<bool> isFiltered(const QString &str, uint64 dialogId) {
if (str.isEmpty()) {
return std::nullopt;
}
const auto icuStr = UnicodeString(reinterpret_cast<const UChar*>(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()) {
if (matches(pattern)) {
return true;
}
}
}
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<PeerData*> peer) {
const auto &settings = AyuSettings::getInstance();
return settings.filtersEnabled && (settings.filtersEnabledInChats || !peer->isMegagroup() && !peer->isGigagroup());
}
bool isBlocked(const not_null<HistoryItem*> item) {
const auto &settings = AyuSettings::getInstance();
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<HistoryMessageForwarded>()) {
if (forwarded->originalSender &&
forwarded->originalSender->isUser() &&
forwarded->originalSender->asUser()->isBlocked()) {
return true;
}
}
return false;
}();
return settings.filtersEnabled &&
(
item->from()->isUser() && ShadowBanUtils::isShadowBanned(getDialogIdFromPeer(item->from())) ||
settings.hideFromBlocked && blocked
);
}
bool filtered(const not_null<HistoryItem*> item) {
const 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), getDialogIdFromPeer(item->history()->peer));
// 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;
}
}
@@ -0,0 +1,46 @@
// 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 <string>
#include <vector>
#include "unicode/regex.h"
using namespace icu_78;
namespace FiltersController {
bool isEnabled(PeerData *peer);
bool isBlocked(not_null<HistoryItem*> item);
bool filteredWithoutCaching(not_null<HistoryItem*> historyItem);
bool filtered(not_null<HistoryItem*> historyItem);
struct ReversiblePattern
{
std::shared_ptr<RegexPattern> pattern;
bool reversed;
};
struct HashablePattern
{
std::vector<char> id;
ReversiblePattern pattern;
bool operator==(const HashablePattern &other) const {
return id == other.id;
}
};
struct PatternHasher
{
std::size_t operator()(const HashablePattern &p) const {
std::string_view view(p.id.data(), p.id.size());
return std::hash<std::string_view>{}(view);
}
};
}
@@ -0,0 +1,629 @@
// 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 <QByteArray>
#include <QClipboard>
#include <QGuiApplication>
#include <QJsonArray>
#include <qjsondocument.h>
#include <QString>
#include <vector>
#include <QtNetwork/QHttpPart>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include "filters_cache_controller.h"
#include "lang_auto.h"
#include "ayu/ayu_settings.h"
#include "ayu/data/ayu_database.h"
#include "ayu/utils/telegram_helpers.h"
#include "data/data_document.h"
#include "data/data_peer.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"
#include "ui/toast/toast.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 auto responseData = _reply->readAll();
const auto jsonString = QString::fromUtf8(responseData);
if (jsonString.isNull()) {
LOG(("FilterUtils: Invalid response."));
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
_reply->deleteLater();
return;
}
if (!handleResponse(jsonString.toUtf8())) {
LOG(("FilterUtils: Error handling response."));
}
_reply->deleteLater();
});
connect(
_reply,
&QNetworkReply::errorOccurred,
this,
[=](QNetworkReply::NetworkError e)
{
gotFailure(e);
_reply->deleteLater();
});
}
void FilterUtils::publishFilters() {
const auto exported = exportFilters();
auto multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart contentPart;
contentPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"content\""));
contentPart.setBody(exported.toUtf8());
QHttpPart syntaxPart;
syntaxPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"syntax\""));
syntaxPart.setBody("json");
QHttpPart titlePart;
titlePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"title\""));
titlePart.setBody("AyuGram Filters");
multiPart->append(contentPart);
multiPart->append(syntaxPart);
multiPart->append(titlePart);
QNetworkRequest request(QUrl("https://dpaste.com/api/v2/"));
_reply = _manager->post(request, multiPart);
multiPart->setParent(_reply);
connect(
_reply,
&QNetworkReply::finished,
this,
[=]
{
const auto error = _reply->error();
const auto location = _reply->header(QNetworkRequest::LocationHeader);
if (error == QNetworkReply::NoError && location.isValid()) {
auto url = location.toString();
url.append(".txt");
QGuiApplication::clipboard()->setText(url);
Ui::Toast::Show(tr::lng_stickers_copied(tr::now));
} else {
LOG(("Failed to publish filters to dpaste, error: %1").arg(_reply->errorString()));
Ui::Toast::Show(tr::ayu_FiltersToastFailPublish(tr::now));
}
_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) {
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
LOG(("FilterUtils: Failed to parse JSON, error: %1"
).arg(error.errorString()));
return false;
}
if (!document.isObject()) {
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
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<char> filterId;
QJsonObject toJson() const {
QJsonObject json;
json["dialogId"] = dialogId;
// make it look like java's UUID
auto hexId = QString(QByteArray(filterId.data(), filterId.size()).toHex());
if (hexId.length() == 32) {
hexId.insert(8, '-');
hexId.insert(13, '-');
hexId.insert(18, '-');
hexId.insert(23, '-');
}
json["filterId"] = hexId;
return json;
}
};
QString FilterUtils::exportFilters() {
auto createJsonArray = [&](const auto &container)
{
QJsonArray jsonArray;
for (const auto &item : container) {
jsonArray.append(item.toJson());
}
return jsonArray;
};
QJsonArray filtersArray;
QJsonObject jsonObject;
jsonObject["version"] = BACKUP_VERSION;
const auto filters = AyuDatabase::getAllRegexFilters();
for (const auto &item : filters) {
QJsonObject filterJson;
filterJson["caseInsensitive"] = item.caseInsensitive;
if (item.dialogId.has_value()) {
filterJson["dialogId"] = item.dialogId.value();
} else {
filterJson["dialogId"] = QJsonValue();
}
filterJson["enabled"] = item.enabled;
filterJson["reversed"] = item.reversed;
filterJson["text"] = QString::fromStdString(item.text);
// make it look like java's UUID
auto hexId = QString(QByteArray(item.id.data(), item.id.size()).toHex());
if (hexId.length() == 32) {
hexId.insert(8, '-');
hexId.insert(13, '-');
hexId.insert(18, '-');
hexId.insert(23, '-');
}
filterJson["id"] = hexId;
filtersArray.append(filterJson);
}
jsonObject["filters"] = filtersArray;
const auto excl = AyuDatabase::getAllFiltersExclusions();
std::vector<BackupExclusion> exclusions;
exclusions.reserve(excl.size());
for (const auto &item : excl) {
exclusions.push_back(BackupExclusion{item.dialogId, item.filterId});
}
jsonObject["exclusions"] = createJsonArray(exclusions);
jsonObject["removeFiltersById"] = QJsonValue();
jsonObject["removeExclusions"] = QJsonValue();
QJsonObject peers;
for (const auto &item : filters) {
const auto &session = currentSession();
if (!item.dialogId.has_value()) {
continue;
}
if (const auto peer = session->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<Data::MediaDice*>(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<HistoryServiceGiveawayResults>()) {
return 28; // TYPE_GIVEAWAY_RESULTS
}
}
return 10; // TYPE_DATE
}
return 0; // TYPE_TEXT
}
QString FilterUtils::extractAllText(not_null<HistoryItem*> 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<HistoryMessageReplyMarkup>()) {
if (!markup->data.isNull()) {
for (const auto &row : markup->data.rows) {
for (const auto &button : row) {
text.append("<button>").append(button.text).append(" ").append(qs(button.data)).append("</button>");
text.append("\n");
}
}
}
}
text.append("\n").append("<type>").append(QString::number(typeOfMessage(item))).append("</type>");
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<RegexFilter> filtersOverrides;
std::map<std::vector<char>, RegexFilter> newFilters;
std::vector<RegexFilterGlobalExclusion> newExclusions;
std::vector<QString> removeFiltersById;
std::vector<RegexFilterGlobalExclusion> removeExclusions;
std::map<long long, QString> 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").toVariant().toLongLong();
} else {
regex.dialogId = std::nullopt;
}
regex.enabled = filter.value("enabled").toBool();
auto idString = filter.value("id").toString();
idString.remove('-');
auto idBytes = QByteArray::fromHex(idString.toUtf8());
regex.id = std::vector(idBytes.constData(), idBytes.constData() + idBytes.size());
regex.reversed = filter.value("reversed").toBool();
regex.text = filter.value("text").toString().toStdString();
auto it = std::ranges::find_if(existingFilters,
[&regex](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").toVariant().toLongLong();
auto filterIdString = exclusion.value("filterId").toString();
filterIdString.remove('-');
auto filterIdBytes = QByteArray::fromHex(filterIdString.toUtf8());
regex.filterId = std::vector(
filterIdBytes.constData(),
filterIdBytes.constData() + filterIdBytes.size()
);
auto it = std::ranges::find_if(
existingExclusions,
[&regex](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<char>(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<char>(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();
crl::on_main([]
{
AyuSettings::fire_filtersUpdate();
});
}
@@ -0,0 +1,73 @@
// 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 <QString>
#include <QtNetwork/QNetworkReply>
#include "ayu/data/entities.h"
#include "core/application.h"
#include "data/data_session.h"
#include "history/history_item_components.h"
struct ApplyChanges
{
std::vector<RegexFilter> newFilters;
std::vector<QString> removeFiltersById;
std::vector<RegexFilter> filtersOverrides;
std::vector<RegexFilterGlobalExclusion> newExclusions;
std::vector<RegexFilterGlobalExclusion> removeExclusions;
std::map<long long, QString> 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);
void publishFilters();
static QString exportFilters();
static QString extractAllText(not_null<HistoryItem *> item);
private:
FilterUtils():
_manager(std::make_unique<QNetworkAccessManager>()) {
}
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<QNetworkAccessManager> _manager = nullptr;
QNetworkReply *_reply = nullptr;
};
@@ -0,0 +1,38 @@
// 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 <QStringList>
#include "filters_cache_controller.h"
#include "ayu/ayu_settings.h"
#include "ayu/data/entities.h"
void ShadowBanUtils::addShadowBan(ID userId) {
auto &settings = AyuSettings::getInstance();
if (settings.shadowBanIds.insert(userId).second) {
setShadowBanList();
}
}
void ShadowBanUtils::removeShadowBan(ID userId) {
auto &settings = AyuSettings::getInstance();
if (settings.shadowBanIds.erase(userId) > 0) {
setShadowBanList();
}
}
bool ShadowBanUtils::isShadowBanned(ID userId) {
const auto &settings = AyuSettings::getInstance();
return settings.shadowBanIds.contains(userId);
}
void ShadowBanUtils::setShadowBanList() {
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
AyuSettings::save();
}
@@ -0,0 +1,21 @@
// 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 <unordered_set>
class ShadowBanUtils {
public:
static void addShadowBan(long long userId);
static void removeShadowBan(long long userId);
static bool isShadowBanned(long long userId);
private:
ShadowBanUtils() = delete;
static void setShadowBanList();
};
@@ -33,7 +33,7 @@ namespace {
const QByteArray &payload,
const QString &iconPath,
int pixel,
int max) {
int max) {
Expects(!payload.isEmpty());
const auto data = Qr::Encode(payload, Qr::Redundancy::Default);
@@ -0,0 +1,140 @@
// 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 "ayu/ui/boxes/import_filters_box.h"
#include "qr/qr_generate.h"
#include "styles/style_boxes.h"
#include "styles/style_intro.h"
#include "styles/style_layers.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/rp_widget.h"
#include "ui/vertical_list.h"
#include "ui/controls/invite_link_label.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include <QtGui/QClipboard>
#include <QtGui/QGuiApplication>
#include <QtSvg/QSvgRenderer>
#include "lang_auto.h"
#include "ayu/features/filters/filters_utils.h"
#include "styles/style_giveaway.h"
#include "styles/style_settings.h"
#include "ui/toast/toast.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
namespace Ui {
void FillImportFiltersBox(not_null<Ui::GenericBox*> box, bool import) {
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
box->setTitle(import ? tr::ayu_FiltersMenuImport() : tr::ayu_FiltersMenuExport());
box->verticalLayout()->resizeToWidth(box->width());
const auto container = box->verticalLayout();
const auto skip = st::settingsSendTypeSkip;
auto wrap = object_ptr<Ui::VerticalLayout>(container);
const auto inner = wrap.data();
container->add(
object_ptr<Ui::OverrideMargins>(
container,
std::move(wrap),
QMargins(0, skip, 0, skip)));
Ui::InputField *importURLField = nullptr;
Ui::SlideWrap<Ui::VerticalLayout> *importURLWrap = nullptr;
const auto intoURL = std::make_shared<RadioenumGroup<bool>>(false);
const auto addOption = [&](bool value, const QString &text)
{
inner->add(
object_ptr<Ui::Radioenum<bool>>(
inner,
intoURL,
value,
text,
st::settingsSendType),
st::settingsSendTypePadding);
if (import && value) {
const auto clipboardText = QGuiApplication::clipboard()->text().trimmed();
const auto prefill = clipboardText.startsWith("http") ? clipboardText : QString();
importURLWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner),
st::giveawayGiftCodeBox.buttonPadding
)
);
importURLField = importURLWrap->entity()->add(
object_ptr<Ui::InputField>(
container,
st::defaultInputField,
rpl::single(QString("URL")),
prefill
)
);
importURLWrap->hide(anim::type::instant);
}
};
addOption(false, import ? tr::ayu_FiltersImportClipboard(tr::now) : tr::ayu_FiltersExportClipboard(tr::now));
addOption(true, import ? tr::ayu_FiltersImportURL(tr::now) : tr::ayu_FiltersExportURL(tr::now));
intoURL->setChangedCallback([=](bool value)
{
if (import) {
importURLWrap->toggle(value, anim::type::normal);
}
});
const auto actionButton = box->addButton(
import ? tr::ayu_FiltersMenuImport() : tr::ayu_FiltersMenuExport(),
[=]
{
const auto isURL = intoURL.get()->current();
if (import) {
if (isURL) {
FilterUtils::getInstance().importFromLink(
importURLField->getLastText().trimmed());
} else {
FilterUtils::getInstance().importFromJson(
QGuiApplication::clipboard()->text().toUtf8());
}
} else {
if (isURL) {
FilterUtils::getInstance().publishFilters();
} else {
const auto data = FilterUtils::getInstance().exportFilters();
QGuiApplication::clipboard()->setText(data);
Toast::Show(tr::lng_text_copied(tr::now));
}
}
box->closeBox();
});
const auto buttonWidth = box->width()
- rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
actionButton->widthValue() | rpl::filter([=]
{
return (actionButton->widthNoMargins() != buttonWidth);
}) | rpl::start_with_next([=]
{
actionButton->resizeToWidth(buttonWidth);
},
actionButton->lifetime());
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
}
} // namespace Ui
@@ -0,0 +1,15 @@
// 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
namespace Ui {
class GenericBox;
void FillImportFiltersBox(not_null<Ui::GenericBox*> box, bool import);
} // namespace Ui
@@ -9,10 +9,10 @@
#include "apiwrap.h"
#include "lang_auto.h"
#include "mainwidget.h"
#include "mainwindow.h"
#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"
@@ -26,6 +26,7 @@
#include "window/window_peer_menu.h"
#include "ayu/ui/message_history/history_section.h"
#include "ayu/ui/settings/filters/edit_filter.h"
#include "ayu/utils/telegram_helpers.h"
#include "base/call_delayed.h"
#include "base/random.h"
@@ -210,6 +211,7 @@ void AddDeletedMessagesActions(PeerData *peerData,
->showSection(std::make_shared<MessageHistory::SectionMemento>(peerData, nullptr, topicId));
},
&st::menuIconArchive);
// todo view filters
}
void AddJumpToBeginningAction(PeerData *peerData,
@@ -313,6 +315,33 @@ 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 = getDialogIdFromPeer(peerData);
const auto shadowBanned = ShadowBanUtils::isShadowBanned(realId);
const auto toggleShadowBan = [=]
{
if (shadowBanned) {
ShadowBanUtils::removeShadowBan(realId);
} else {
ShadowBanUtils::addShadowBan(realId);
}
};
addCallback({
.text = (shadowBanned
? tr::ayu_FiltersQuickUnshadowBan(tr::now)
: tr::ayu_FiltersQuickShadowBan(tr::now)),
.handler = toggleShadowBan,
.icon = shadowBanned ? &st::menuIconShowInChat : &st::menuIconStealth,
});
}
void AddDeleteOwnMessagesAction(PeerData *peerData,
Data::ForumTopic *topic,
not_null<Window::SessionController*> sessionController,
@@ -718,4 +747,30 @@ void AddBurnAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
&st::menuIconTTLAny);
}
void AddCreateFilterAction(not_null<Ui::PopupMenu*> menu,
not_null<Window::SessionController*> controller,
HistoryItem *item,
const QString &selectedText) {
const auto &settings = AyuSettings::getInstance();
if (!needToShowItem(settings.showAddFilterInContextMenu) || !settings.filtersEnabled) {
return;
}
if (!item || selectedText.isEmpty()) {
return;
}
menu->addAction(
tr::ayu_RegexFilterQuickAdd(tr::now),
[=]
{
RegexFilter filter;
filter.text = selectedText.toStdString();
filter.reversed = false;
controller->show(Settings::RegexEditBox(&filter, {}, getDialogIdFromPeer(item->history()->peer), true));
},
&st::menuIconAddToFolder);
}
} // namespace AyuUi
@@ -25,11 +25,13 @@ void AddJumpToBeginningAction(PeerData *peerData,
not_null<Window::SessionController*> sessionController,
const Window::PeerMenuCallback &addCallback);
void AddShadowBanAction(PeerData *peerData,
const Window::PeerMenuCallback &addCallback);
void AddOpenChannelAction(PeerData *peerData,
not_null<Window::SessionController*> sessionController,
const Window::PeerMenuCallback &addCallback);
not_null<Window::SessionController*> sessionController,
const Window::PeerMenuCallback &addCallback);
void AddDeleteOwnMessagesAction(PeerData *peerData,
Data::ForumTopic* topic,
Data::ForumTopic *topic,
not_null<Window::SessionController*> sessionController,
const Window::PeerMenuCallback &addCallback);
@@ -39,5 +41,9 @@ void AddUserMessagesAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddMessageDetailsAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddReadUntilAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddBurnAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddCreateFilterAction(not_null<Ui::PopupMenu*> menu,
not_null<Window::SessionController*> controller,
HistoryItem *item,
const QString &selectedText);
}
@@ -10,6 +10,7 @@
using "ui/basic.style";
using "ui/layers/layers.style";
using "ui/widgets/widgets.style";
using "info/info.style";
centeredBoxLabelStyle: TextStyle(boxLabelStyle) {
font: font(13px);
@@ -21,3 +22,17 @@ centeredBoxLabel: FlatLabel(defaultFlatLabel) {
align: align(center);
style: centeredBoxLabelStyle;
}
filtersAddIcon: IconButton(infoLayerTopBarClose) {
width: 40px;
icon: icon {{ "menu/add", boxTitleCloseFg }};
iconOver: icon {{ "menu/add", boxTitleCloseFgOver }};
iconPosition: point(8px, -1px);
}
filtersExcludeIcon: IconButton(infoLayerTopBarClose) {
width: 40px;
icon: icon {{ "menu/tag_remove", boxTitleCloseFg }};
iconOver: icon {{ "menu/tag_remove", boxTitleCloseFgOver }};
iconPosition: point(8px, -1px);
}
@@ -0,0 +1,266 @@
// 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 "lang_auto.h"
#include "ayu/ayu_settings.h"
#include "ayu/data/ayu_database.h"
#include "ayu/features/filters/filters_cache_controller.h"
#include "base/event_filter.h"
#include "base/platform/base_platform_info.h"
#include "boxes/delete_messages_box.h"
#include "core/mime_type.h"
#include "media/audio/media_audio.h"
#include "media/view/media_view_pip.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
#include "styles/style_window.h"
#include "ui/ui_utility.h"
#include "ui/boxes/confirm_box.h"
#include "ui/effects/animations.h"
#include "ui/text/text.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
namespace Settings {
std::vector<char> generate_uuid_bytes() {
// stolen somewhere from Internet
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<uint32_t> dist;
std::vector<uint8_t> 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<char>(bytes.begin(), bytes.end());
}
bool validateRegex(const icu::UnicodeString& pattern, std::string& errorMsg) {
UErrorCode status = U_ZERO_ERROR;
UParseError parseError;
icu::RegexPattern* regexPattern = icu::RegexPattern::compile(
pattern,
0, // flags
parseError,
status
);
if (U_FAILURE(status)) {
auto errorCodeNormalized = std::string(u_errorName(status));
errorCodeNormalized = errorCodeNormalized.substr(8); // skip U_REGEX_
std::ranges::transform(
errorCodeNormalized,
errorCodeNormalized.begin(),
[](unsigned char c)
{
if (c == '_') {
return std::tolower(' ');
}
return std::tolower(c);
});
errorCodeNormalized[0] = std::toupper(errorCodeNormalized[0]);
errorMsg = errorCodeNormalized + " at " + std::to_string(parseError.offset);
if (parseError.preContext[0] != 0 || parseError.postContext[0] != 0) {
icu::UnicodeString pre(parseError.preContext);
icu::UnicodeString post(parseError.postContext);
std::string preStr, postStr;
pre.toUTF8String(preStr);
post.toUTF8String(postStr);
errorMsg += " (near: '" + preStr + "' -> '" + postStr + "')";
}
delete regexPattern;
return false;
}
delete regexPattern;
return true;
}
not_null<Ui::SlideWrap<Ui::FlatLabel>*> AddError(
not_null<Ui::VerticalLayout*> content,
Ui::InputField *input) {
std::string errorText;
validateRegex(icu::UnicodeString::fromUTF8("("), errorText);
const auto error = content->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
content,
object_ptr<Ui::FlatLabel>(
content,
QString::fromStdString(errorText) + QString::fromStdString(errorText),
st::settingLocalPasscodeError), st::settingsCheckboxPadding));
error->hide(anim::type::instant);
if (input) {
input->changes() | rpl::start_with_next(
[=]
{
error->hide(anim::type::normal);
},
input->lifetime());
}
return error;
};
void RegexEditBuilder(
not_null<Ui::GenericBox*> box,
RegexFilter *filter,
const Fn<void(RegexFilter)> &onDone,
std::optional<long long> 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 regexValue = box->addRow(
object_ptr<Ui::InputField>(
box->verticalLayout(),
st::windowFilterNameInput,
Ui::InputField::Mode::MultiLine,
tr::ayu_RegexFiltersPlaceholder()),
st::markdownLinkFieldPadding);
const auto errorText = AddError(box->verticalLayout(), regexValue);
const auto enabled = box->addRow(
object_ptr<Ui::Checkbox>(
box,
tr::ayu_EnableExpression(tr::now),
data.enabled,
st::defaultBoxCheckbox),
st::settingsCheckboxPadding);
const auto caseInsensitive = box->addRow(
object_ptr<Ui::Checkbox>(
box,
tr::ayu_CaseInsensitiveExpression(tr::now),
data.caseInsensitive,
st::defaultBoxCheckbox),
st::settingsCheckboxPadding);
const auto reversed = box->addRow(
object_ptr<Ui::Checkbox>(
box,
tr::ayu_ReversedExpression(tr::now),
data.reversed,
st::defaultBoxCheckbox),
st::settingsCheckboxPadding);
regexValue->setText(QString::fromStdString(data.text));
auto saveAndClose = [=, id = data.id]
{
const auto text = regexValue->getTextWithTags().text;
if (text.isEmpty()) {
return;
}
std::string error;
if (!validateRegex(icu::UnicodeString::fromUTF8(text.toStdString()), error)) {
errorText->entity()->setText(QString::fromStdString(error));
errorText->show(anim::type::normal);
return;
}
RegexFilter newFilter;
newFilter.text = regexValue->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;
};
// todo: custom toast with "Move to shared" button
// based on `PaidReactionToast`
Ui::Toast::Show(Ui::Toast::Config{
.text = tr::ayu_RegexFilterBulletinText(
tr::now,
Ui::Text::RichLangValue
),
.filter = onClick,
.adaptive = true
});
}
});
});
};
regexValue->submits() | rpl::start_with_next(saveAndClose, regexValue->lifetime());
box->addButton(tr::lng_settings_save(), saveAndClose);
box->addButton(tr::lng_cancel(),
[=]
{
box->closeBox();
});
errorText->entity()->resizeToWidth(box->width());
errorText->resizeToWidth(box->width());
}
object_ptr<Ui::GenericBox> RegexEditBox(RegexFilter *filter,
const Fn<void(RegexFilter)> &onDone,
std::optional<long long> dialogId,
bool showToast) {
return Box(RegexEditBuilder, filter, onDone, dialogId, showToast);
}
} // namespace Settings
@@ -0,0 +1,28 @@
// 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 "boxes/premium_limits_box.h"
#include "base/unixtime.h"
class BoxContent;
namespace Window {
class Controller;
class SessionController;
} // namespace Window
namespace Settings {
object_ptr<Ui::GenericBox> RegexEditBox(RegexFilter *filter,
const Fn<void(RegexFilter)> &onDone,
std::optional<long long> dialogId = std::nullopt,
bool showToast = false);
} // namespace Settings
@@ -0,0 +1,161 @@
// 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 "per_dialog_filter.h"
#include <utility>
#include "lang_auto.h"
#include "settings_filters_list.h"
#include "ayu/data/ayu_database.h"
#include "ayu/utils/telegram_helpers.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "ui/painter.h"
#include "window/window_session_controller.h"
namespace Settings {
PerDialogFiltersListRow::PerDialogFiltersListRow(PeerId peer)
: PeerListRow(peer.value)
, peerId(peer) {
}
QString PerDialogFiltersListRow::generateName() {
if (const auto from = getPeerFromDialogId(peerId.value & PeerId::kChatTypeMask)) {
this->setPeer(from);
return PeerListRow::generateName();
}
return QString("UNKNOWN (ID: %1)").arg(QString::number(peerId.value & PeerId::kChatTypeMask));
}
PaintRoundImageCallback PerDialogFiltersListRow::generatePaintUserpicCallback(bool forceRound) {
if (const auto from = getPeerFromDialogId(peerId.value & PeerId::kChatTypeMask)) {
this->setPeer(from);
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>(
EmptyUserpic::UserpicColor(realId % 7),
QString("U")); // U - Unknown
_userpicEmpty->paintCircle(p, x, y, outerWidth, size);
};
}
PerDialogFiltersListController::PerDialogFiltersListController(not_null<Main::Session*> session,
not_null<Window::SessionController*> controller)
: _session(session)
, _controller(controller) {
}
Main::Session &PerDialogFiltersListController::session() const {
return *_session;
}
void PerDialogFiltersListController::prepare() {
const auto filters = AyuDatabase::getAllRegexFilters();
const auto exclusions = AyuDatabase::getAllFiltersExclusions();
if (filters.empty() && exclusions.empty()) {
return;
}
countsByDialogIds.clear();
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(abs(id)));
auto row = std::make_unique<PerDialogFiltersListRow>(peerId);
auto status = QString();
if (count.filters > 0) {
status += tr::ayu_RegexFiltersAmount(tr::now, lt_count, count.filters);
if (count.exclusions > 0) {
status += ", ";
}
}
if (count.exclusions > 0) {
status += tr::ayu_RegexFiltersExcludedAmount(tr::now, lt_count, count.exclusions);
}
row->setCustomStatus(status, false);
delegate()->peerListAppendRow(reinterpret_cast<std::unique_ptr<PeerListRow>&&>(row));
}
// sortByName();
delegate()->peerListRefreshRows();
}
void PerDialogFiltersListController::rowClicked(not_null<PeerListRow*> peer) {
ID did;
if (peer->special()) {
const ID pred = peer->id() & PeerId::kChatTypeMask;
if (countsByDialogIds.contains(pred)) {
did = pred;
} else {
did = -pred;
}
} else {
did = getDialogIdFromPeer(peer->peer());
}
_controller->dialogId = did;
_controller->showExclude = true;
_controller->showSettings(AyuFiltersList::Id());
}
SelectChatBoxController::SelectChatBoxController(
not_null<Window::SessionController*> controller,
Fn<void(not_null<PeerData*>)> onSelectedCallback)
: ChatsListBoxController(&controller->session())
, _controller(controller)
, _onSelectedCallback(std::move(onSelectedCallback)) {
}
Main::Session &SelectChatBoxController::session() const {
return _controller->session();
}
void SelectChatBoxController::rowClicked(not_null<PeerListRow*> row) {
if (_onSelectedCallback) {
_onSelectedCallback(row->peer());
}
}
std::unique_ptr<ChatsListBoxController::Row> SelectChatBoxController::createRow(not_null<History*> history) {
const auto peer = history->peer;
const auto skip =
peer->isUser() ||
//peer->forum() ||
peer->monoforum();
if (skip) {
return nullptr;
}
auto result = std::make_unique<Row>(
history,
nullptr);
return result;
}
void SelectChatBoxController::prepareViewHook() {
delegate()->peerListSetTitle(tr::ayu_FiltersMenuSelectChat());
}
}
@@ -0,0 +1,79 @@
// 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 "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 PerDialogFiltersListRow final : public PeerListRow
{
public:
explicit PerDialogFiltersListRow(PeerId peer);
QString generateName() override;
PaintRoundImageCallback generatePaintUserpicCallback(bool forceRound) override;
private:
PeerId peerId;
};
class PerDialogFiltersListController final : public PeerListController
{
public:
explicit PerDialogFiltersListController(not_null<Main::Session*> session,
not_null<Window::SessionController*> controller);
[[nodiscard]] Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
private:
struct FilterCounts
{
int filters = 0;
int exclusions = 0;
};
std::unordered_map<ID, FilterCounts> countsByDialogIds;
const not_null<Main::Session*> _session;
not_null<Window::SessionController*> _controller;
};
class SelectChatBoxController
: public ChatsListBoxController
, public base::has_weak_ptr
{
public:
explicit SelectChatBoxController(
not_null<Window::SessionController*> controller,
Fn<void(not_null<PeerData*>)> onSelectedCallback
);
Main::Session &session() const override;
void rowClicked(not_null<PeerListRow*> row) override;
protected:
std::unique_ptr<Row> createRow(not_null<History*> history) override;
void prepareViewHook() override;
private:
not_null<Window::SessionController*> _controller;
Fn<void(not_null<PeerData*>)> _onSelectedCallback;
};
} // namespace Settings
@@ -0,0 +1,269 @@
// 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 <styles/style_layers.h>
#include <styles/style_media_view.h>
#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 "ayu/utils/telegram_helpers.h"
#include "data/data_channel.h"
#include "info/info_wrap_widget.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/vertical_layout.h"
#include "window/window_session_controller.h"
namespace Settings {
rpl::producer<QString> AyuFiltersList::title() {
if (!dialogId.has_value()) {
return tr::ayu_RegexFiltersShared();
}
const auto did = abs(dialogId.value());
const auto from = getPeerFromDialogId(did);
// todo: shorten based on available space
// because it may break on custom fonts
QString res;
if (from) {
auto name = from->topBarNameText();
if (name.length() > 18) {
name = name.left(17) + "";
}
res = name;
} else {
res = tr::ayu_RegexFiltersHeader(tr::now) + " (" + QString::number(did) + ")";
}
return rpl::single(res);
}
AyuFiltersList::AyuFiltersList(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent), _controller(controller), _content(Ui::CreateChild<Ui::VerticalLayout>(this)) {
if (_controller->dialogId.has_value()) {
dialogId = _controller->dialogId.value();
}
setupContent(controller);
}
void AyuFiltersList::checkBeforeClose(Fn<void()> close) {
_controller->showExclude = true;
close();
}
void AyuFiltersList::addNewFilter(const RegexFilter &filter, bool exclusion) {
const auto state = lifetime().make_state<RegexFilter>(filter);
const auto button = _content->add(
object_ptr<Button>(
_content,
rpl::single(QString::fromStdString(state->text).replace("\n", " ")),
st::settingsButtonNoIcon
)
);
if (!state->enabled) {
button->setColorOverride(st::storiesComposeGrayText->c);
}
auto defaultClickHandler = [=, this]() mutable
{
auto _contextMenu = new Ui::PopupMenu(this, st::popupMenuWithIcons);
_contextMenu->setAttribute(Qt::WA_DeleteOnClose);
_contextMenu->addAction(
tr::lng_theme_edit(tr::now),
[=, this]
{
_controller->show(
RegexEditBox(
state,
nullptr
));
},
&st::menuIconEdit);
_contextMenu->addAction(
state->enabled ? tr::lng_settings_auto_night_disable(tr::now) : tr::lng_sure_enable(tr::now),
[=]
{
state->enabled = !state->enabled;
AyuDatabase::updateRegexFilter(*state);
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
},
state->enabled ? &st::menuIconBlock : &st::menuIconUnblock);
_contextMenu->addSeparator();
_contextMenu->addAction(
tr::lng_theme_delete(tr::now),
[=]
{
AyuDatabase::deleteFilter(state->id);
AyuDatabase::deleteExclusionsByFilterId(state->id);
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
},
&st::menuIconDelete);
_contextMenu->popup(QCursor::pos());
};
// we've opened filters list from top "Exclude" button
// on click, close the section
auto exclusionsClickHandler = [=, controller = _controller, dialogId = dialogId]() mutable
{
Expects(dialogId.has_value());
/*
└── class Info::WrapWidget
└── class Info::Settings::Widget
└── class Ui::ScrollArea
└── class QWidget
└── class Ui::PaddingWrap<class Ui::RpWidget>
└── class Settings::AyuFiltersList
*/
// controller->showBackFromStack() doesn't work (closes box completely)
// so as a workaround, use WrapWidget
const auto wrap = dynamic_cast<Info::WrapWidget*>(parent()->parent()->parent()->parent()->parent());
const RegexFilterGlobalExclusion newExclusion = {
.dialogId = dialogId.value(),
.filterId = state->id
};
AyuDatabase::addRegexExclusion(newExclusion);
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
controller->dialogId = dialogId;
controller->showExclude = true;
wrap->showBackFromStackInternal(Window::SectionShow(anim::type::normal));
};
auto deleteExclusionsClickHandler = [=, this]() mutable
{
auto _contextMenu = new Ui::PopupMenu(this, st::popupMenuWithIcons);
_contextMenu->setAttribute(Qt::WA_DeleteOnClose);
_contextMenu->addAction(
tr::lng_theme_delete(tr::now),
[=, this]
{
Expects(dialogId.has_value());
AyuDatabase::deleteExclusion(dialogId.value(), state->id);
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
},
&st::menuIconDelete);
_contextMenu->popup(QCursor::pos());
};
if (exclusion) {
button->addClickHandler(deleteExclusionsClickHandler);
} else if (dialogId.has_value() && _controller->showExclude.has_value() && !_controller->showExclude.value()) {
button->addClickHandler(exclusionsClickHandler);
} else {
button->addClickHandler(defaultClickHandler);
}
crl::on_main(
this,
[=, this]
{
adjustSize();
updateGeometry();
});
}
void AyuFiltersList::initializeSharedFilters(
not_null<Ui::VerticalLayout*> container) {
if (dialogId.has_value() && _controller->showExclude.has_value() && _controller->showExclude.value()) {
filters = AyuDatabase::getByDialogId(dialogId.value());
exclusions = AyuDatabase::getExcludedByDialogId(dialogId.value());
} else {
filters = AyuDatabase::getShared();
// remove shared filters that already excluded for that peer exclusion
if (dialogId.has_value() && _controller->showExclude.has_value() && !_controller->showExclude.value()) {
const auto excludedForDialogId = AyuDatabase::getExcludedByDialogId(dialogId.value());
auto rangeToRemove = std::ranges::remove_if(
filters,
[&](const RegexFilter &filter)
{
for (const auto &excluded : excludedForDialogId) {
if (excluded == filter) {
return true;
}
}
return false;
});
filters.erase(rangeToRemove.begin(), rangeToRemove.end());
}
}
if (!filters.empty()) {
AddSkip(container);
filtersTitle = AddSubsectionTitle(container, tr::ayu_RegexFiltersHeader());
for (const auto &filter : filters) {
addNewFilter(filter);
}
}
if (!exclusions.empty()) {
if (!filters.empty()) {
AddSkip(container);
AddDivider(container);
AddSkip(container);
}
excludedTitle = AddSubsectionTitle(container, tr::ayu_RegexFiltersExcluded());
for (const auto &exclusion : exclusions) {
addNewFilter(exclusion, true);
}
}
if (filters.empty() && exclusions.empty()) {
Ui::AddDividerText(container, tr::ayu_RegexFiltersListEmpty());
}
}
void AyuFiltersList::setupContent(not_null<Window::SessionController*> controller) {
initializeSharedFilters(_content);
ResizeFitChild(this, _content);
}
} // namespace Settings
@@ -0,0 +1,51 @@
// 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.h"
#include "settings/settings_common_session.h"
#include "ui/layers/box_content.h"
#include "ui/widgets/popup_menu.h"
class BoxContent;
namespace Window {
class Controller;
class SessionController;
} // namespace Window
namespace Settings {
class AyuFiltersList : public Section<AyuFiltersList>
{
public:
AyuFiltersList(QWidget *parent, not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<QString> title() override;
void checkBeforeClose(Fn<void()> close) override;
private:
void setupContent(not_null<Window::SessionController*> controller);
void initializeSharedFilters(not_null<Ui::VerticalLayout*> container);
void addNewFilter(const RegexFilter &filter, bool exclusion = false);
not_null<Window::SessionController*> _controller;
not_null<Ui::VerticalLayout*> _content;
std::vector<RegexFilter> filters;
std::vector<RegexFilter> exclusions;
Ui::FlatLabel *filtersTitle = nullptr;
Ui::FlatLabel *excludedTitle = nullptr;
std::optional<long long> dialogId;
};
} // namespace Settings
@@ -9,6 +9,8 @@
#include "lang_auto.h"
#include "settings_ayu_utils.h"
#include "ayu/ayu_settings.h"
#include "boxes/peer_list_box.h"
#include "filters/settings_filters_list.h"
#include "settings/settings_common.h"
#include "styles/style_settings.h"
#include "ui/vertical_list.h"
@@ -242,31 +244,6 @@ void SetupSpyEssentials(not_null<Ui::VerticalLayout*> container) {
container->lifetime());
}
void SetupMessageFilters(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
AddSubsectionTitle(container, tr::ayu_RegexFilters());
AddButtonWithIcon(
container,
tr::ayu_FiltersHideFromBlocked(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->hideFromBlocked)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->hideFromBlocked);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_hideFromBlocked(enabled);
AyuSettings::save();
},
container->lifetime());
}
void SetupOther(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
@@ -327,12 +304,6 @@ void AyuGhost::setupContent(not_null<Window::SessionController*> controller) {
AddDivider(content);
AddSkip(content);
SetupMessageFilters(content);
AddSkip(content);
AddDivider(content);
AddSkip(content);
SetupOther(content);
AddSkip(content);
@@ -24,10 +24,14 @@ class PainterHighQualityEnabler;
namespace Settings {
QString asBeta(const QString &text) {
return text + QString(" β");
}
rpl::producer<QString> asBeta(rpl::producer<QString> text) {
return std::move(text) | rpl::map([=](const QString &val)
{
return val + " β";
return asBeta(val);
});
}
@@ -24,6 +24,7 @@ struct NestedEntry
std::function<void(bool)> callback;
};
QString asBeta(const QString &text);
rpl::producer<QString> asBeta(rpl::producer<QString> text);
not_null<Ui::RpWidget*> AddInnerToggle(not_null<Ui::VerticalLayout*> container,
@@ -469,6 +469,21 @@ void SetupContextMenuElements(not_null<Ui::VerticalLayout*> container,
AyuSettings::set_showMessageDetailsInContextMenu(index);
AyuSettings::save();
});
if (settings->filtersEnabled) {
AddChooseButtonWithIconAndRightText(
container,
controller,
settings->showAddFilterInContextMenu,
options,
tr::ayu_RegexFilterQuickAdd(),
tr::ayu_SettingsContextMenuTitle(),
st::menuIconAddToFolder,
[=](int index)
{
AyuSettings::set_showAddFilterInContextMenu(index);
AyuSettings::save();
});
}
AddSkip(container);
AddDividerText(container, tr::ayu_SettingsContextMenuDescription());
@@ -652,4 +667,4 @@ void AyuChats::setupContent(not_null<Window::SessionController*> controller) {
ResizeFitChild(this, content);
}
} // namespace Settings
} // namespace Settings
@@ -0,0 +1,281 @@
// 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.h"
#include "lang_auto.h"
#include "ayu/ayu_settings.h"
#include "ayu/data/ayu_database.h"
#include "ayu/features/filters/filters_cache_controller.h"
#include "ayu/ui/boxes/import_filters_box.h"
#include "ayu/utils/telegram_helpers.h"
#include "boxes/abstract_box.h"
#include "boxes/peer_list_box.h"
#include "core/application.h"
#include "filters/per_dialog_filter.h"
#include "filters/settings_filters_list.h"
#include "inline_bots/bot_attach_web_view.h"
#include "settings/settings_common.h"
#include "styles/style_boxes.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include "ui/vertical_list.h"
#include "ui/boxes/confirm_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_controller.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
namespace Settings {
rpl::producer<QString> AyuFilters::title() {
return tr::ayu_CategoryFilters();
}
void AyuFilters::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
addAction(
tr::ayu_FiltersMenuSelectChat(tr::now),
[=]
{
if (const auto window = Core::App().activeWindow()) {
if (const auto controller = window->sessionController()) {
auto types = InlineBots::PeerTypes();
types |= InlineBots::PeerType::Bot;
types |= InlineBots::PeerType::Group;
types |= InlineBots::PeerType::Broadcast;
Window::ShowChooseRecipientBox(
controller,
[=](not_null<Data::Thread*> thread)
{
const auto peer = thread->peer();
controller->dialogId = getDialogIdFromPeer(peer);
controller->showExclude = true;
controller->showSettings(AyuFiltersList::Id());
return true;
},
tr::ayu_FiltersMenuSelectChat(),
nullptr,
types
);
}
}
},
&st::menuIconSearch);
addAction({
.isSeparator = true
});
addAction(
tr::ayu_FiltersMenuImport(tr::now),
[=]
{
auto box = Box(Ui::FillImportFiltersBox, true);
Ui::show(std::move(box));
},
&st::menuIconArchive);
if (AyuDatabase::hasFilters()) {
addAction(
tr::ayu_FiltersMenuExport(tr::now),
[=]
{
auto box = Box(Ui::FillImportFiltersBox, false);
Ui::show(std::move(box));
},
&st::menuIconUnarchive);
}
addAction({
.isSeparator = true
});
addAction(
tr::ayu_FiltersMenuClear(tr::now),
[=]
{
auto callback = [=](Fn<void()> &&close)
{
AyuDatabase::deleteAllFilters();
AyuDatabase::deleteAllExclusions();
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
close();
};
auto box = Ui::MakeConfirmBox({
.text = tr::ayu_FiltersClearPopupText(),
.confirmed = callback,
.confirmText = tr::ayu_FiltersClearPopupActionText()
});
Ui::show(std::move(box));
},
&st::menuIconClear);
}
AyuFilters::AyuFilters(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent) {
setupContent(controller);
}
void SetupFiltersSettings(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
AddSkip(container);
AddSubsectionTitle(container, tr::ayu_RegexFilters());
AddButtonWithIcon(
container,
tr::ayu_RegexFiltersEnable(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->filtersEnabled)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->filtersEnabled);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_filtersEnabled(enabled);
AyuSettings::save();
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
},
container->lifetime());
AddButtonWithIcon(
container,
tr::ayu_RegexFiltersEnableSharedInChats(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->filtersEnabledInChats)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->filtersEnabledInChats);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_filtersEnabledInChats(enabled);
AyuSettings::save();
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
},
container->lifetime());
AddButtonWithIcon(
container,
tr::ayu_FiltersHideFromBlocked(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->hideFromBlocked)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->hideFromBlocked);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_hideFromBlocked(enabled);
AyuSettings::save();
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
},
container->lifetime());
AddSkip(container);
}
void SetupShared(not_null<Window::SessionController*> controller,
Ui::VerticalLayout *container) {
Ui::AddSkip(container);
auto button = container->add(object_ptr<Ui::SettingsButton>(
container,
tr::ayu_RegexFiltersShared()
));
button->addClickHandler([=]
{
controller->dialogId = std::nullopt; // ensure we're handling shared filters
controller->showExclude = false;
controller->showSettings(AyuFiltersList::Id());
});
}
void SetupPerDialog(
not_null<Window::SessionController*> controller,
not_null<Ui::VerticalLayout*> container
) {
auto ctrl = container->lifetime().make_state<PerDialogFiltersListController>(
&controller->session(),
controller
);
auto list = object_ptr<Ui::PaddingWrap<PeerListContent>>(
container,
object_ptr<PeerListContent>(
container,
ctrl),
QMargins(0, -st::peerListBox.padding.top(), 0, -st::peerListBox.padding.bottom()));
AddSkip(container);
const auto content = container->add(std::move(list));
AddSkip(container);
auto delegate = container->lifetime().make_state<PeerListContentDelegateSimple>();
delegate->setContent(content->entity());
ctrl->setDelegate(delegate);
}
void SetupMessageFilters(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
AddSubsectionTitle(container, tr::ayu_RegexFilters());
AddButtonWithIcon(
container,
tr::ayu_FiltersHideFromBlocked(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->hideFromBlocked)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->hideFromBlocked);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_hideFromBlocked(enabled);
AyuSettings::save();
},
container->lifetime());
}
void AyuFilters::setupContent(not_null<Window::SessionController*> controller) {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
SetupFiltersSettings(content);
AddDivider(content);
SetupShared(controller, content);
if (AyuDatabase::hasPerDialogFilters()) {
AddSkip(content);
AddDivider(content);
SetupPerDialog(controller, content);
}
ResizeFitChild(this, content);
}
} // namespace Settings
@@ -0,0 +1,30 @@
// 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 "settings/settings_common.h"
#include "settings/settings_common_session.h"
namespace Window {
class SessionController;
} // namespace Window
namespace Settings {
class AyuFilters : public Section<AyuFilters>
{
public:
AyuFilters(QWidget *parent, not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<QString> title() override;
void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;
private:
void setupContent(not_null<Window::SessionController*> controller);
};
} // namespace Settings
@@ -26,4 +26,4 @@ private:
void setupContent(not_null<Window::SessionController*> controller);
};
} // namespace Settings
} // namespace Settings
@@ -12,7 +12,9 @@
#include "lang_auto.h"
#include "settings_appearance.h"
#include "settings_ayu.h"
#include "settings_ayu_utils.h"
#include "settings_chats.h"
#include "settings_filters.h"
#include "settings_general.h"
#include "settings_other.h"
@@ -87,7 +89,7 @@ void SetupCategories(
const auto categories = std::vector<CategoryInfo>{
{QString("AyuGram"), &st::menuIconGroupReactions, [=] { showOther(AyuGhost::Id()); }},
// {QString("Filters"), &st::menuIconTagFilter, [=] { showOther(AyuFilters::Id()); }},
{asBeta(tr::ayu_CategoryFilters(tr::now)), &st::menuIconTagFilter, [=] { showOther(AyuFilters::Id()); }},
{tr::ayu_CategoryGeneral(tr::now), &st::menuIconShowAll, [=] { showOther(AyuGeneral::Id()); }},
{tr::ayu_CategoryAppearance(tr::now), &st::menuIconPalette, [=] { showOther(AyuAppearance::Id()); }},
{tr::ayu_CategoryChats(tr::now), &st::menuIconChatBubble, [=] { showOther(AyuChats::Id()); }},
@@ -247,4 +247,4 @@ void AyuOther::setupContent(not_null<Window::SessionController*> controller) {
ResizeFitChild(this, content);
}
} // namespace Settings
} // namespace Settings
@@ -7,6 +7,7 @@
#include "telegram_helpers.h"
#include <functional>
#include <latch>
#include <QTimer>
#include "apiwrap.h"
@@ -38,9 +39,12 @@
#include "ayu/ayu_state.h"
#include "ayu/data/messages_storage.h"
#include "data/data_poll.h"
#include "ayu/features/filters/filters_controller.h"
#include "data/data_saved_sublist.h"
#include "main/main_domain.h"
#include "unicode/regex.h"
namespace {
constexpr auto usernameResolverBotId = 8001593505L;
@@ -132,24 +136,7 @@ bool isMessageHidden(const not_null<HistoryItem*> item) {
return true;
}
const auto &settings = AyuSettings::getInstance();
if (settings.hideFromBlocked) {
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<HistoryMessageForwarded>()) {
if (forwarded->originalSender &&
forwarded->originalSender->isUser() &&
forwarded->originalSender->asUser()->isBlocked()) {
return true;
}
}
}
return false;
return FiltersController::filtered(item);
}
void MarkAsReadChatList(not_null<Dialogs::MainList*> list) {
@@ -812,3 +799,73 @@ bool mediaDownloadable(const Data::Media *media) {
}
return true;
}
void resolveAllChats(const std::map<long long, QString> &peers) {
// not sure is this works
auto session = currentSession();
crl::async([=, &session]
{
while (!peers.empty()) {
for (const auto &[id, username] : peers) {
std::latch latch(1);
auto onSuccess = [=, &latch](const MTPChatInvite &invite) {
invite.match([=](const MTPDchatInvite &data) {},
[=](const MTPDchatInviteAlready &data) {
if (const auto chat = session->data().processChat(data.vchat())) {
if (const auto channel = chat->asChannel()) {
channel->clearInvitePeek();
}
}
}, [=](const MTPDchatInvitePeek &data) {});
latch.count_down();
};
auto onFail = [=, &latch](const MTP::Error &error) {
if (MTP::IsFloodError(error.type())) {
std::this_thread::sleep_for(std::chrono::seconds(20));
}
latch.count_down();
};
session->api().checkChatInvite(username, onSuccess, onFail);
latch.wait();
}
}
});
}
not_null<Main::Session *> currentSession() {
return &Core::App().domain().active().session();
}
template<typename T>
PeerData* getPeerFromDialogId(T id) {
for (const auto &[index, account] : Core::App().domain().accounts()) {
if (const auto session = account->maybeSession()) {
PeerData *from = session->data().userLoaded(id);
if (!from) {
from = session->data().channelLoaded(id);
}
if (!from) {
from = reinterpret_cast<PeerData*>(session->data().chatLoaded(id));
}
if (from) {
return from;
}
}
}
return nullptr;
}
PeerData* getPeerFromDialogId(ID id) {
return getPeerFromDialogId<ID>(id);
}
PeerData* getPeerFromDialogId(unsigned long long id) {
return getPeerFromDialogId<unsigned long long>(id);
}
@@ -61,4 +61,10 @@ void searchById(ID userId, Main::Session *session, const UsernameResolverCallbac
ID getUserIdFromPackId(uint64 id);
TextWithTags extractText(not_null<HistoryItem*> item);
bool mediaDownloadable(const Data::Media* media);
bool mediaDownloadable(const Data::Media* media);
void resolveAllChats(const std::map<long long, QString> &peers);
not_null<Main::Session *> currentSession();
PeerData* getPeerFromDialogId(ID id);
PeerData* getPeerFromDialogId(unsigned long long id);
@@ -85,6 +85,12 @@ public:
return _peer;
}
// AyuGram
void setPeer(not_null<PeerData*> peer) {
_peer = peer;
}
// AyuGram
[[nodiscard]] PeerListRowId id() const {
return _id;
}
@@ -107,7 +107,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
// AyuGram includes
#include "ayu/ayu_settings.h"
#include "ayu/features/filters/filters_cache_controller.h"
#include "ayu/ui/context_menu/context_menu.h"
#include "ayu/ui/settings/filters/edit_filter.h"
#include "ayu/utils/telegram_helpers.h"
#include "data/data_document_media.h"
@@ -2340,6 +2342,9 @@ void HistoryInner::contextMenuEvent(QContextMenuEvent *e) {
}
void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
const auto &settings = AyuSettings::getInstance();
if (e->reason() == QContextMenuEvent::Mouse) {
mouseActionUpdate(e->globalPos());
}
@@ -2985,6 +2990,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
hasCopyRestrictionForSelected()));
}, &st::menuIconTranslate);
}
AyuUi::AddCreateFilterAction(_menu, _controller, item, selectedText.rich.text);
addItemActions(item, item);
} else {
addReplyAction(partItemOrLeader);
@@ -63,7 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
// AyuGram includes
#include "ayu/ayu_settings.h"
#include "ayu/features/filters/filters_controller.h"
namespace {
@@ -536,16 +536,21 @@ void HistoryMessageReply::updateData(
&& author->isUser()
&& author->asUser()->isBlocked();
const auto filtered = resolvedMessage &&
!resolvedMessage.empty() &&
FiltersController::filtered(resolvedMessage.get());
const auto displaying = resolvedMessage
|| resolvedStory
|| ((nonEmptyQuote || _fields.externalMedia)
&& (!_fields.messageId || force));
_displaying = displaying && !blocked ? 1 : 0;
_displaying = displaying && !blocked && !filtered ? 1 : 0;
const auto unavailable = !resolvedMessage
&& !resolvedStory
&& ((!_fields.storyId && !_fields.messageId) || force);
_unavailable = unavailable && !blocked ? 1 : 0;
_unavailable = unavailable && !blocked && !filtered ? 1 : 0;
if (force) {
if (!_displaying && (_fields.messageId || _fields.storyId)) {
@@ -658,10 +658,10 @@ HistoryWidget::HistoryWidget(
}, lifetime());
rpl::merge(
AyuSettings::get_hideFromBlockedReactive() | rpl::to_empty,
session().changes().peerUpdates(
Data::PeerUpdate::Flag::IsBlocked
) | rpl::to_empty
) | rpl::to_empty,
AyuSettings::get_filtersUpdate()
) | rpl::start_with_next(
[=]
{
@@ -676,6 +676,20 @@ HistoryWidget::HistoryWidget(
}
updateHistoryGeometry();
update();
for (const auto &item : _history->blocks) {
if (!item) {
continue;
}
for (const auto &msg : item->messages) {
if (!msg) {
continue;
}
_history->owner().requestViewResize(msg.get());
_history->owner().requestItemViewRefresh(msg->data());
}
}
}
});
},
@@ -1592,7 +1592,7 @@ void Element::destroyUnreadBar() {
}
int Element::displayedDateHeight() const {
if (AyuFeatures::MessageShot::isTakingShot()) {
if (AyuFeatures::MessageShot::isTakingShot() || isMessageHidden(data())) {
return 0;
}
@@ -2124,10 +2124,10 @@ Element *Element::previousInBlocks() const {
Element *Element::previousDisplayedInBlocks() const {
auto result = previousInBlocks();
while (result && (result->data()->isEmpty() || result->isHidden())) {
while (result && ((result->data()->isEmpty() || result->isHidden()) && !isMessageHidden(data()))) {
result = result->previousInBlocks();
}
return result;
return result == this ? nullptr : result;
}
Element *Element::nextInBlocks() const {
@@ -2145,10 +2145,10 @@ Element *Element::nextInBlocks() const {
Element *Element::nextDisplayedInBlocks() const {
auto result = nextInBlocks();
while (result && (result->data()->isEmpty() || result->isHidden())) {
while (result && ((result->data()->isEmpty() || result->isHidden()) && !isMessageHidden(data()))) {
result = result->nextInBlocks();
}
return result;
return result == this ? nullptr : result;
}
void Element::drawInfo(
@@ -22,7 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
// AyuGram includes
#include "ayu/ayu_settings.h"
#include "ayu/features/filters/shadow_ban_utils.h"
namespace HistoryView {
namespace {
@@ -73,6 +73,9 @@ bool SendActionPainter::updateNeedsAnimating(
return false;
}
}
if (ShadowBanUtils::isShadowBanned(user->id.value & PeerId::kChatTypeMask)) {
return false;
}
const auto now = crl::now();
const auto emplaceAction = [&](
@@ -53,6 +53,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_menu_icons.h"
#include "styles/style_layers.h"
// AyuGram includes
#include "ayu/ayu_settings.h"
#include "ayu/ui/settings/filters/edit_filter.h"
#include "ayu/ui/settings/filters/settings_filters_list.h"
#include "styles/style_ayu_settings.h"
namespace Info {
namespace {
@@ -410,6 +415,31 @@ void WrapWidget::setupTopBarMenuToggle() {
Box(Ui::FillPeerQrBox, self, std::nullopt, nullptr));
});
}
} else if (section.settingsType() == ::Settings::AyuFiltersList::Id()) {
const auto controller = _controller->parentController();
const auto &st = st::filtersAddIcon;
const auto button = _topBar->addButton(base::make_unique_q<Ui::IconButton>(_topBar, st));
const auto show = controller->uiShow();
button->addClickHandler(
[=]
{
show->show(::Settings::RegexEditBox(nullptr, nullptr, controller->dialogId));
});
if (controller->showExclude.has_value() && controller->showExclude.value()) {
auto icon = base::make_unique_q<Ui::IconButton>(_topBar, st::filtersExcludeIcon);
const auto excludeButton = _topBar->addButton(std::move(icon));
excludeButton->addClickHandler([=, content = _content.data()]
{
// open new
controller->showExclude = false;
controller->showSettings(::Settings::AyuFiltersList::Id());
});
}
}
} else if (key.storiesPeer()
&& key.storiesPeer()->isSelf()
@@ -680,6 +710,21 @@ void WrapWidget::finishShowContent() {
) | rpl::start_with_next([=] {
updateContentGeometry();
}, _content->lifetime());
AyuSettings::get_filtersUpdate() | rpl::start_with_next([=]
{
auto contentMemento = _content->createMemento();
if (!contentMemento) {
return;
}
std::vector<std::shared_ptr<ContentMemento>> stack;
stack.push_back(std::move(contentMemento));
const auto sectionMemento = std::make_shared<Memento>(std::move(stack));
showBackFromStackInternal(Window::SectionShow(anim::type::instant));
showInternal(sectionMemento.get(), Window::SectionShow(anim::type::instant));
}, _content->lifetime());
}
rpl::producer<bool> WrapWidget::topShadowToggledValue() const {
@@ -11,12 +11,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_chat.h"
#include "settings/settings_main.h"
// AyuGram includes
#include "ayu/ui/settings/settings_filters.h"
namespace Settings {
bool HasMenu(Type type) {
return (type == ::Settings::CloudPasswordEmailConfirmId())
|| (type == Main::Id())
|| (type == Chat::Id());
|| (type == Chat::Id())
|| (type == AyuFilters::Id());
}
} // namespace Settings
@@ -1579,6 +1579,7 @@ void Filler::fillProfileActions() {
addManageTopic();
addToggleTopicClosed();
AyuUi::AddOpenChannelAction(_peer, _controller, _addAction);
AyuUi::AddShadowBanAction(_peer, _addAction);
addViewDiscussion();
addDirectMessages();
addExportChat();
@@ -678,6 +678,10 @@ public:
return _lifetime;
}
// AyuGram filters
std::optional<long long> dialogId;
std::vector<char> filterId;
std::optional<bool> showExclude; // whether to show exclude button in the top bar
private:
struct CachedThemeKey;
struct CachedTheme;
+1
Submodule Telegram/lib_icu added at 6c828925ef