feat(WIP): message filters

This commit is contained in:
bleizix 2025-09-14 23:24:36 +05:00
parent 4acc69e6ec
commit 8a1fdf2943
40 changed files with 2746 additions and 28 deletions

View file

@ -148,6 +148,12 @@ set(ayugram_files
ayu/ui/settings/settings_main.h
ayu/ui/settings/settings_other.cpp
ayu/ui/settings/settings_other.h
ayu/ui/settings/filters/settings_filters_list.cpp
ayu/ui/settings/filters/settings_filters_list.h
ayu/ui/settings/filters/peer_global_exclusion.cpp
ayu/ui/settings/filters/peer_global_exclusion.h
ayu/ui/settings/filters/edit_filter.cpp
ayu/ui/settings/filters/edit_filter.h
ayu/ui/context_menu/context_menu.cpp
ayu/ui/context_menu/context_menu.h
ayu/ui/context_menu/menu_item_subtext.cpp
@ -205,6 +211,14 @@ set(ayugram_files
ayu/features/translator/implementations/telegram.h
ayu/features/translator/implementations/base.cpp
ayu/features/translator/implementations/base.h
ayu/features/filters/filters_controller.cpp
ayu/features/filters/filters_controller.h
ayu/features/filters/filters_cache_controller.cpp
ayu/features/filters/filters_cache_controller.h
ayu/features/filters/filters_utils.cpp
ayu/features/filters/filters_utils.h
ayu/features/filters/shadow_ban_utils.cpp
ayu/features/filters/shadow_ban_utils.h
ayu/data/messages_storage.cpp
ayu/data/messages_storage.h
ayu/data/entities.h

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

View file

@ -7000,6 +7000,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_FiltersHideFromBlockedNote" = "Message filters were enabled.";
"ayu_RegexFiltersEnableSharedInChats" = "Enable Shared Filters in Chats";
"ayu_RegexFiltersAdd" = "Add Filter";
"ayu_RegexFiltersAddDescription" = "You can use {link} to test and debug your regular expression.";
"ayu_RegexFiltersAddDescription_link" = "regex101.com";
"ayu_RegexFiltersEdit" = "Edit Filter";
"ayu_RegexFiltersPlaceholder" = "Expression";
"ayu_EnableExpression" = "Enable Filter";

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"
@ -52,12 +53,16 @@ void initTranslator() {
Ayu::Translator::TranslateManager::init();
}
void initShadowBan() {
ShadowBanUtils::reloadShadowBan();
}
void init() {
initLang();
initDatabase();
initUiSettings();
initWorker();
initRCManager();
initShadowBan();
initTranslator();
}

View file

@ -44,7 +44,13 @@ rpl::variable<int> showPeerIdReactive;
rpl::variable<QString> translationProviderReactive;
rpl::variable<bool> filtersEnabledReactive;
rpl::variable<bool> filtersEnabledInChatsReactive;
rpl::variable<QString> shadowBanIdsReactive;
rpl::variable<bool> hideFromBlockedReactive;
rpl::event_stream<> filtersUpdateReactive; // triggered on adding / editing filter
rpl::event_stream<> historyUpdateReactive;
rpl::lifetime lifetime = rpl::lifetime();
@ -142,6 +148,9 @@ void postinitialize() {
showPeerIdReactive = settings->showPeerId;
translationProviderReactive = settings->translationProvider;
filtersEnabledReactive = settings->filtersEnabled;
filtersEnabledInChatsReactive = settings->filtersEnabledInChats;
shadowBanIdsReactive = settings->shadowBanIds;
hideFromBlockedReactive = settings->hideFromBlocked;
ghostModeEnabled = ghostModeEnabled_util(settings.value());
@ -226,6 +235,8 @@ AyuGramSettings::AyuGramSettings() {
saveForBots = false;
// ~ Message filters
filtersEnabled = false;
filtersEnabledInChats = false;
hideFromBlocked = false;
// ~ QoL toggles
@ -397,6 +408,20 @@ void set_saveForBots(bool val) {
settings->saveForBots = val;
}
void set_filtersEnabled(bool val) {
settings->filtersEnabled = val;
filtersEnabledReactive = val;
}
void set_filtersEnabledInChats(bool val) {
settings->filtersEnabledInChats = val;
filtersEnabledInChatsReactive = val;
}
void set_shadowBanIds(const QString &val) {
settings->shadowBanIds = val;
shadowBanIdsReactive = val;
}
void set_hideFromBlocked(bool val) {
settings->hideFromBlocked = val;
hideFromBlockedReactive = val;
@ -694,9 +719,25 @@ rpl::producer<bool> get_ghostModeEnabledReactive() {
return ghostModeEnabled.value();
}
rpl::producer<bool> get_filtersEnabledReactive() {
return filtersEnabledReactive.value();
}
rpl::producer<bool> get_filtersEnabledInChatsReactive() {
return filtersEnabledInChatsReactive.value();
}
rpl::producer<QString> get_shadowBanIdsReactive() {
return shadowBanIdsReactive.value();
}
rpl::producer<bool> get_hideFromBlockedReactive() {
return hideFromBlockedReactive.value();
}
void fire_filtersUpdate() {
filtersUpdateReactive.fire({});
}
rpl::producer<> get_filtersUpdate() {
return filtersUpdateReactive.events();
}
void triggerHistoryUpdate() {
historyUpdateReactive.fire({});

View file

@ -59,6 +59,9 @@ public:
bool saveForBots;
QString shadowBanIds;
bool filtersEnabled;
bool filtersEnabledInChats;
bool hideFromBlocked;
bool disableAds;
@ -160,6 +163,9 @@ void set_saveMessagesHistory(bool val);
void set_saveForBots(bool val);
void set_filtersEnabled(bool val);
void set_filtersEnabledInChats(bool val);
void set_shadowBanIds(const QString &val);
void set_hideFromBlocked(bool val);
void set_disableAds(bool val);
@ -255,6 +261,9 @@ inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nloh
NLOHMANN_JSON_TO(saveDeletedMessages)
NLOHMANN_JSON_TO(saveMessagesHistory)
NLOHMANN_JSON_TO(saveForBots)
NLOHMANN_JSON_TO(shadowBanIds)
NLOHMANN_JSON_TO(filtersEnabled)
NLOHMANN_JSON_TO(filtersEnabledInChats)
NLOHMANN_JSON_TO(hideFromBlocked)
NLOHMANN_JSON_TO(disableAds)
NLOHMANN_JSON_TO(disableStories)
@ -334,6 +343,9 @@ inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nl
NLOHMANN_JSON_FROM_WITH_DEFAULT(saveDeletedMessages)
NLOHMANN_JSON_FROM_WITH_DEFAULT(saveMessagesHistory)
NLOHMANN_JSON_FROM_WITH_DEFAULT(saveForBots)
NLOHMANN_JSON_FROM_WITH_DEFAULT(filtersEnabled)
NLOHMANN_JSON_FROM_WITH_DEFAULT(filtersEnabledInChats)
NLOHMANN_JSON_FROM_WITH_DEFAULT(shadowBanIds)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideFromBlocked)
NLOHMANN_JSON_FROM_WITH_DEFAULT(disableAds)
NLOHMANN_JSON_FROM_WITH_DEFAULT(disableStories)
@ -418,8 +430,14 @@ bool isUseScheduledMessages();
rpl::producer<bool> get_ghostModeEnabledReactive();
rpl::producer<bool> get_filtersEnabledReactive();
rpl::producer<bool> get_filtersEnabledInChatsReactive();
rpl::producer<QString> get_shadowBanIdsReactive();
rpl::producer<bool> get_hideFromBlockedReactive();
void fire_filtersUpdate();
rpl::producer<> get_filtersUpdate();
void triggerHistoryUpdate();
rpl::producer<> get_historyUpdateReactive();

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,165 @@ 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()));
}
}
}

View file

@ -20,4 +20,28 @@ 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();
}

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);
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

View file

@ -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();
}
}

View file

@ -0,0 +1,29 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#include "filters_controller.h"
#include "ayu/data/entities.h"
using namespace FiltersController;
namespace FiltersCacheController {
void rebuildCache();
std::unordered_map<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();
}

View file

@ -0,0 +1,188 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#include "filters_controller.h"
#include "filters_cache_controller.h"
#include "ayu/ayu_settings.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item.h"
#include "unicode/regex.h"
#include <functional>
#include <QTimer>
#include "apiwrap.h"
#include "ayu/data/entities.h"
#include "core/mime_type.h"
#include "data/data_channel.h"
#include "data/data_peer_id.h"
#include "data/data_session.h"
#include "history/history_item_components.h"
#include "filters_utils.h"
#include "shadow_ban_utils.h"
namespace FiltersController {
bool filterBlocked(const not_null<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;
}
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()) {
return matches(pattern);
}
}
const auto &exclusions = FiltersCacheController::getExclusionsByDialogId(dialogId);
if (const auto &sharedPatterns = FiltersCacheController::getSharedPatterns(); !sharedPatterns.empty()) {
for (const auto &pattern : sharedPatterns) {
if (exclusions.has_value() && exclusions.value().contains(pattern)) {
continue;
}
if (matches(pattern.pattern)) {
return true;
}
}
}
return false;
}
bool isEnabled(not_null<PeerData*> peer) {
auto &settings = AyuSettings::getInstance();
return settings.filtersEnabled && (settings.filtersEnabledInChats || peer->asChannel());
}
bool isBlocked(const not_null<HistoryItem *> item) {
auto &settings = AyuSettings::getInstance();
ID peer = 0;
if (const auto user = item->from()->asUser()) {
peer = user->id.value & PeerId::kChatTypeMask;
}
const auto blocked = [&]() -> bool
{
if (item->from()->isUser() &&
item->from()->asUser()->isBlocked()) {
// don't hide messages if it's a dialog with blocked user
return item->from()->asUser()->id != item->history()->peer->id;
}
if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
if (forwarded->originalSender &&
forwarded->originalSender->isUser() &&
forwarded->originalSender->asUser()->isBlocked()) {
return true;
}
}
return false;
}();
return settings.filtersEnabled &&
(
ShadowBanUtils::isShadowBanned(peer) ||
settings.hideFromBlocked && blocked
);
}
// unused, probably need to remove
bool filteredWithoutCaching(const not_null<HistoryItem *> item) {
auto &settings = AyuSettings::getInstance();
if (!settings.filtersEnabled) {
return false;
}
if (item->out()) {
return false;
}
if (filterBlocked(item)) return true;
if (!isEnabled(item->from())) return false;
const auto cached = FiltersCacheController::isFiltered(item);
if (cached.has_value()) {
return cached.value();
}
const auto filtered = isFiltered(FilterUtils::extractAllText(item), item->id.bare);
if (filtered.has_value()) {
return filtered.value();
}
return false;
}
// Main Method
bool filtered(const not_null<HistoryItem *> item) {
auto &settings = AyuSettings::getInstance();
if (!settings.filtersEnabled) {
return false;
}
if (item->out()) {
return false;
}
if (filterBlocked(item)) return true;
if (!isEnabled(item->history()->peer)) return false;
const auto cached = FiltersCacheController::isFiltered(item);
if (cached.has_value()) {
return cached.value();
}
const auto res = isFiltered(FilterUtils::extractAllText(item), item->history()->peer->id.value & PeerId::kChatTypeMask);
// sometimes item has empty text.
// so we cache result only if
// processed item is filterable
if (res.has_value()) {
FiltersCacheController::putFiltered(item, res.value());
return res.value();
}
return false;
}
}

View file

@ -0,0 +1,52 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#pragma once
#include <vector>
#include <string>
#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;
// && pattern.pattern == other.pattern.pattern
// && pattern.reversed == other.pattern.reversed;
}
};
// im unable to override std::hash of HashablePattern (skill issue) so use hasher
struct PatternHasher
{
std::size_t operator()(const HashablePattern &p) const {
std::string_view view(p.id.data(), p.id.size());
return std::hash<std::string_view>{}(view);
}
};
}

View file

@ -0,0 +1,509 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#include "filters_utils.h"
#include <lang_auto.h>
#include <QJsonArray>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include "ayu/data/ayu_database.h"
#include "ui/toast/toast.h"
#include <QString>
#include <QByteArray>
#include <vector>
#include "filters_cache_controller.h"
#include "ayu/ayu_settings.h"
#include "ayu/utils/telegram_helpers.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "history/history_item.h"
#include "main/main_account.h"
#include "main/main_domain.h"
#include "main/main_session.h"
constexpr auto BACKUP_VERSION = 2;
void FilterUtils::importFromLink(const QString &link) {
if (link.isEmpty()) {
Ui::Toast::Show(tr::ayu_FiltersToastFailFetch(tr::now));
return;
}
const auto request = QNetworkRequest(QUrl(link));
_reply = _manager->get(request);
connect(_reply, &QNetworkReply::finished, this, [=]{
const QByteArray response = _reply->readAll();
if (!handleResponse(response)) {
LOG(("Filters import: Error handling response or bad map size: %1").arg(response.size()));
}
_reply->deleteLater();
});
connect(_reply, &QNetworkReply::errorOccurred, this, [=](QNetworkReply::NetworkError e) {
gotFailure(e);
_reply->deleteLater();
});
}
bool FilterUtils::importFromJson(const QByteArray &json) {
auto error = QJsonParseError{0, QJsonParseError::NoError};
const auto document = QJsonDocument::fromJson(json, &error);
if (error.error != QJsonParseError::NoError) {
LOG(("FilterUtils: Failed to parse JSON, error: %1"
).arg(error.errorString()));
return false;
}
if (!document.isObject()) {
LOG(("FilterUtils: not an object received in JSON"));
return false;
}
const auto changes = prepareChanges(document.object());
if (changes == ApplyChanges{}) {
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
return false;
}
const auto any = !changes.newFilters.empty() || !changes.removeFiltersById.empty() || !changes.filtersOverrides.empty() || !changes.newExclusions.empty() || !changes.removeExclusions.empty() || !changes.peersToBeResolved.empty();
if (!any) {
Ui::Toast::Show(tr::ayu_FiltersToastFailNoChanges(tr::now));
return false;
}
applyChanges(changes);
return true;
}
struct BackupExclusion
{
ID dialogId;
std::vector<char> filterId;
QJsonObject toJson() const {
QJsonObject json;
json["dialogId"] = dialogId;
json["filterId"] = QString::fromUtf8(filterId);
return json;
}
};
QString FilterUtils::exportFilters() {
auto createJsonArray = [&](const auto &container) {
QJsonArray jsonArray;
for (const auto &item : container) {
jsonArray.append(item.toJson());
}
return jsonArray;
};
QJsonObject jsonObject;
jsonObject["version"] = BACKUP_VERSION;
const auto filters = AyuDatabase::getAllRegexFilters();
jsonObject["filters"] = createJsonArray(filters);
const auto excl = AyuDatabase::getAllFiltersExclusions();
std::vector<BackupExclusion> exclusions;
exclusions.reserve(excl.size());
for (const auto &item : excl) {
exclusions.push_back(BackupExclusion{item.dialogId, item.filterId});
}
jsonObject["exclusions"] = createJsonArray(exclusions);
QJsonObject peers;
for (const auto &item : filters) {
const auto &currentSession = Core::App().domain().active().session();
if (!item.dialogId.has_value()) {
continue;
}
if (const auto peer = currentSession.data().peer(peerFromChat(abs(item.dialogId.value())))) {
if (!peer->username().isEmpty()) {
QString key = QString::number(item.dialogId.value());
peers[key] = peer->username();
}
}
}
jsonObject["peers"] = peers;
QJsonDocument jsonDoc(jsonObject);
QByteArray jsonData = jsonDoc.toJson(QJsonDocument::Indented);
return QString::fromUtf8(jsonData);
}
// for compatibility with Android version
int typeOfMessage(const HistoryItem *item) {
if (item->isSponsored()) {
return 0; // TYPE_TEXT
}
if (!item->isService()) {
if (const auto media = item->media()) {
if (const auto invoice = media->invoice(); invoice && invoice->isPaidMedia) {
return 29; // TYPE_PAID_MEDIA
}
if (media->giveawayStart()) {
return 26; // TYPE_GIVEAWAY
}
if (media->giveawayResults()) {
return 28; // TYPE_GIVEAWAY_RESULTS
}
if (const auto dice = dynamic_cast<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").toInteger();
} else {
regex.dialogId = std::nullopt;
}
regex.enabled = filter.value("enabled").toBool();
auto byteArray = filter.value("id").toString().toUtf8();
regex.id = std::vector(byteArray.constData(), byteArray.constData() + byteArray.size());
regex.reversed = filter.value("reversed").toBool();
regex.text = filter.value("text").toString().toStdString();
auto it = std::ranges::find_if(existingFilters,
[&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").toInteger();
auto byteArray = exclusion.value("filterId").toString().toUtf8();
regex.filterId = std::vector(byteArray.constData(), byteArray.constData() + byteArray.size());
auto it = std::ranges::find_if(existingExclusions,
[&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();
}

View file

@ -0,0 +1,83 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#pragma once
#include <QString>
#include <QtNetwork/QNetworkReply>
#include "ayu/data/entities.h"
#include "core/application.h"
#include "api/api_bot.h"
#include "core/click_handler_types.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item_components.h"
#include "main/main_session.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_widgets.h"
struct ApplyChanges
{
std::vector<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);
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;
};

View file

@ -0,0 +1,64 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#include "shadow_ban_utils.h"
#include <QStringList>
#include "filters_cache_controller.h"
#include "ayu/ayu_settings.h"
#include "ayu/data/entities.h"
std::unordered_set<ID> ShadowBanUtils::shadowBanList;
void ShadowBanUtils::reloadShadowBan() {
loadShadowBanList();
}
void ShadowBanUtils::addShadowBan(ID userId) {
if (shadowBanList.insert(userId).second) {
setShadowBanList();
}
}
void ShadowBanUtils::removeShadowBan(ID userId) {
if (shadowBanList.erase(userId) > 0) {
setShadowBanList();
}
}
bool ShadowBanUtils::isShadowBanned(ID userId) {
return shadowBanList.contains(userId);
}
void ShadowBanUtils::loadShadowBanList() {
auto &settings = AyuSettings::getInstance();
if (settings.shadowBanIds.isEmpty()) {
return;
}
const auto idList = settings.shadowBanIds.split(',', Qt::SkipEmptyParts);
for (const auto &id : idList) {
shadowBanList.insert(id.toLongLong());
}
}
const std::unordered_set<ID> &ShadowBanUtils::getShadowBanList() {
return shadowBanList;
}
void ShadowBanUtils::setShadowBanList() {
QStringList idStringList;
idStringList.reserve(shadowBanList.size());
for (const auto &id : shadowBanList) {
idStringList.push_back(QString::number(id));
}
FiltersCacheController::rebuildCache();
AyuSettings::set_shadowBanIds(idStringList.join(","));
AyuSettings::save();
}

View file

@ -0,0 +1,24 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#pragma once
#include <unordered_set>
class ShadowBanUtils {
public:
static void reloadShadowBan();
static void addShadowBan(long long userId);
static void removeShadowBan(long long userId);
static bool isShadowBanned(long long userId);
private:
ShadowBanUtils() = delete;
static const std::unordered_set<long long> &getShadowBanList();
static void loadShadowBanList();
static void setShadowBanList();
static std::unordered_set<long long> shadowBanList;
};

View file

@ -10,6 +10,7 @@
using "ui/colors.palette";
using "ui/widgets/widgets.style";
using "dialogs/dialogs.style";
using "info/info.style";
ayuGhostIcon: icon {{ "ayu/ghost", menuIconColor }};
ayuMenuIcon: icon {{ "ayu/ayu_menu", menuIconColor }};
@ -52,3 +53,15 @@ dialogsExteraSupporterIcon: ThreeStateIcon {
over: icon {{ "ayu/dialogs_extera_supporter", dialogsVerifiedIconBgOver }};
active: icon {{ "ayu/dialogs_extera_supporter", dialogsVerifiedIconBgActive }};
}
ayuFiltersAddIcon: IconButton(infoLayerTopBarClose) {
width: 40px;
icon: icon {{ "menu/add", boxTitleCloseFg }};
iconOver: icon {{ "menu/add", boxTitleCloseFgOver }};
iconPosition: point(8px, -1px);
}
ayuFiltersExcludeIcon: IconButton(infoLayerTopBarClose) {
width: 40px;
icon: icon {{ "ayu/streamer", boxTitleCloseFg }};
iconOver: icon {{ "ayu/streamer", boxTitleCloseFgOver }};
iconPosition: point(8px, -1px);
}

View file

@ -13,6 +13,7 @@
#include "ayu/ayu_settings.h"
#include "ayu/ayu_state.h"
#include "ayu/data/messages_storage.h"
#include "ayu/features/filters/shadow_ban_utils.h"
#include "ayu/ui/context_menu/menu_item_subtext.h"
#include "ayu/utils/qt_key_modifiers_extended.h"
#include "history/history_item_components.h"
@ -210,6 +211,7 @@ void AddDeletedMessagesActions(PeerData *peerData,
->showSection(std::make_shared<MessageHistory::SectionMemento>(peerData, nullptr, topicId));
},
&st::menuIconArchive);
// todo view filters
}
void AddJumpToBeginningAction(PeerData *peerData,
@ -313,6 +315,32 @@ void AddOpenChannelAction(PeerData *peerData,
&st::menuIconChannel);
}
void AddShadowBanAction(PeerData *peerData,
const Window::PeerMenuCallback &addCallback) {
const auto &settings = AyuSettings::getInstance();
if (!peerData || !peerData->isUser() || !settings.filtersEnabled) {
return;
}
const auto realId = peerData->id.value & PeerId::kChatTypeMask;
const auto toggleShadowBan = [=]
{
if (ShadowBanUtils::isShadowBanned(realId)) {
ShadowBanUtils::removeShadowBan(realId);
} else {
ShadowBanUtils::addShadowBan(realId);
}
};
addCallback({
.text = (ShadowBanUtils::isShadowBanned(realId)
? tr::ayu_FiltersQuickUnshadowBan(tr::now)
: tr::ayu_FiltersQuickShadowBan(tr::now)),
.handler = toggleShadowBan,
.icon = &st::menuIconStealth,
});
}
void AddDeleteOwnMessagesAction(PeerData *peerData,
Data::ForumTopic *topic,
not_null<Window::SessionController*> sessionController,

View file

@ -25,6 +25,8 @@ 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);

View file

@ -0,0 +1,434 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#include "edit_filter.h"
#include <styles/style_layers.h>
#include <ui/widgets/fields/masked_input_field.h>
#include <styles/style_window.h>
#include <ui/toast/toast.h>
#include <ui/text/text_utilities.h>
#include "ayu/ayu_settings.h"
#include "lang_auto.h"
#include "boxes/connection_box.h"
#include "styles/style_boxes.h"
#include "styles/style_settings.h"
#include "ayu/data/ayu_database.h"
#include "ayu/features/filters/filters_cache_controller.h"
#include "ayu/utils/telegram_helpers.h"
#include "ui/qt_object_factory.h"
#include "ui/ui_utility.h"
#include "ui/vertical_list.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
#include "ui/widgets/fields/password_input.h"
#include "ui/widgets/labels.h"
#include "base/event_filter.h"
#include "ui/effects/animations.h"
#include "styles/style_window.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/text/text.h"
#include "media/view/media_view_overlay_widget.h"
#include "apiwrap.h"
#include "api/api_attached_stickers.h"
#include "api/api_peer_photo.h"
#include "base/qt/qt_common_adapters.h"
#include "base/timer_rpl.h"
#include "lang/lang_keys.h"
#include "menu/menu_sponsored.h"
#include "boxes/premium_preview_box.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "core/file_utilities.h"
#include "core/mime_type.h"
#include "core/ui_integration.h"
#include "core/crash_reports.h"
#include "core/sandbox.h"
#include "core/shortcuts.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/buttons.h"
#include "ui/layers/layer_manager.h"
#include "ui/text/text_utilities.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/toast/toast.h"
#include "ui/text/format_values.h"
#include "ui/item_text_options.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/power_saving.h"
#include "ui/cached_round_corners.h"
#include "ui/gl/gl_window.h"
#include "ui/boxes/confirm_box.h"
#include "ui/ui_utility.h"
#include "info/info_memento.h"
#include "info/info_controller.h"
#include "info/statistics/info_statistics_widget.h"
#include "boxes/delete_messages_box.h"
#include "boxes/report_messages_box.h"
#include "media/audio/media_audio.h"
#include "media/view/media_view_group_thumbs.h"
#include "media/view/media_view_pip.h"
#include "media/view/media_view_overlay_raster.h"
#include "media/view/media_view_overlay_opengl.h"
#include "media/view/media_view_playback_sponsored.h"
#include "media/stories/media_stories_share.h"
#include "media/stories/media_stories_view.h"
#include "media/streaming/media_streaming_document.h"
#include "media/streaming/media_streaming_player.h"
#include "media/player/media_player_instance.h"
#include "history/history.h"
#include "history/history_item_helpers.h"
#include "history/view/media/history_view_media.h"
#include "history/view/reactions/history_view_reactions_selector.h"
#include "data/components/sponsored_messages.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_media_rotation.h"
#include "data/data_photo_media.h"
#include "data/data_document_media.h"
#include "data/data_document_resolver.h"
#include "data/data_file_click_handler.h"
#include "data/data_download_manager.h"
#include "window/themes/window_theme_preview.h"
#include "window/window_peer_menu.h"
#include "window/window_controller.h"
#include "base/platform/base_platform_info.h"
#include "base/power_save_blocker.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "base/qt_signal_producer.h"
#include "base/event_filter.h"
#include "main/main_account.h"
#include "main/main_domain.h" // Domain::activeSessionValue.
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "layout/layout_document_generic_preview.h"
#include "platform/platform_overlay_widget.h"
#include "storage/file_download.h"
#include "storage/storage_account.h"
#include "calls/calls_instance.h"
#include "styles/style_media_view.h"
#include "styles/style_calls.h"
#include "styles/style_chat.h"
#include "styles/style_menu_icons.h"
#include <QtWidgets/QApplication>
#include <QtCore/QBuffer>
#include <QtGui/QGuiApplication>
#include <QtGui/QWindow>
#include <QtGui/QScreen>
#include <kurlmimedata.h>
class PainterHighQualityEnabler;
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());
}
rpl::producer<QString> AyuEditFilters::title()
{
return tr::ayu_RegexFiltersAdd();
}
object_ptr<Ui::Checkbox> getCheckBox(not_null<Ui::VerticalLayout *> container,
const QString &label,
bool checked)
{
return object_ptr<Ui::Checkbox>(
container,
label,
checked,
st::settingsCheckbox);
}
AyuEditFilters::AyuEditFilters(
QWidget *parent,
not_null<Window::SessionController *> controller)
: Section(parent)
{
if (!controller->filterId.empty()) {
currentFilter = AyuDatabase::getById(controller->filterId);
}
setupContent(controller, parent);
}
// unused currently, TODO: need to add this on bad regex pattern
not_null<Ui::FlatLabel *> AddError(
not_null<Ui::VerticalLayout *> content,
Ui::PasswordInput *input)
{
const auto error = content->add(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
content,
object_ptr<Ui::FlatLabel>(
content,
// Set any text to resize.
tr::lng_language_name(tr::now),
st::settingLocalPasscodeError)),
st::changePhoneDescriptionPadding)->entity();
error->hide();
if (input) {
QObject::connect(input,
&Ui::MaskedInputField::changed,
[=]
{
error->hide();
});
}
return error;
};
void AyuEditFilters::setupSettings(not_null<Ui::VerticalLayout *> container, QWidget *parent)
{
AddSkip(container);
const auto add = [&](const QString &label, bool checked, auto &&handle)
{
auto check = container->add(
getCheckBox(container, label, checked),
st::settingsCheckboxPadding
);
check->checkedChanges(
) | rpl::start_with_next(
std::forward<decltype(handle)>(handle),
container->lifetime());
return check;
};
const auto name = container->add(
object_ptr<Ui::InputField>(
parent,
st::windowFilterNameInput,
Ui::InputField::Mode::MultiLine,
tr::ayu_RegexFiltersPlaceholder()),
st::markdownLinkFieldPadding);
auto enabled = add(
QString("Enable Filter"),
currentFilter.enabled,
[=, this](bool checked)
{
currentFilter.enabled = checked;
});
auto insensetive = add(
QString("Case Insensitive"),
currentFilter.caseInsensitive,
[=, this](bool checked)
{
currentFilter.caseInsensitive = checked;
});
auto reversed = add(
QString("Reversed"),
currentFilter.reversed,
[=, this](bool checked)
{
currentFilter.reversed = checked;
});
name->setText(QString::fromStdString(currentFilter.text));
name->submits(
) | rpl::start_with_next([=, this]
{
currentFilter.text = name->getTextWithTags().text.toStdString();
currentFilter.enabled = enabled->checked();
currentFilter.caseInsensitive = insensetive->checked();
currentFilter.reversed = reversed->checked();
if (currentFilter.id.empty()) {
currentFilter.id = generate_uuid_bytes();
}
AyuDatabase::addRegexFilter(currentFilter);
FiltersCacheController::rebuildCache();
},
name->lifetime());
}
void RegexEditBuilder(
not_null<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 name = box->addRow(
object_ptr<Ui::InputField>(
box->verticalLayout(),
st::windowFilterNameInput,
Ui::InputField::Mode::MultiLine,
tr::ayu_RegexFiltersPlaceholder()),
st::markdownLinkFieldPadding);
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);
name->setText(QString::fromStdString(data.text));
auto saveAndClose = [=, id = data.id]
{
RegexFilter newFilter;
newFilter.text = name->getTextWithTags().text.toStdString();
newFilter.enabled = enabled->checked();
newFilter.caseInsensitive = caseInsensitive->checked();
newFilter.reversed = reversed->checked();
if (!showToast && dialogId.has_value()) {
newFilter.dialogId = dialogId;
}
if (!id.empty()) {
newFilter.id = id;
} else {
newFilter.id = generate_uuid_bytes();
}
box->closeBox();
crl::async([=]
{
AyuDatabase::addRegexFilter(newFilter);
FiltersCacheController::rebuildCache();
crl::on_main([=]
{
if (onDone) {
onDone(newFilter);
}
AyuSettings::fire_filtersUpdate();
if (showToast) {
const auto onClick = [=](const auto &...) mutable{
newFilter.dialogId = dialogId;
AyuDatabase::updateRegexFilter(newFilter);
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
return true;
};
Ui::Toast::Show(Ui::Toast::Config{
// .text = tr::ayu_RegexFilterBulletinText(
// tr::now,
// lt_link,
// Ui::Text::Link(
// Ui::Text::Bold(
// tr::ayu_RegexFilterBulletinAction(tr::now))),
// Ui::Text::RichLangValue),
// TODO: reconsider
.text = tr::ayu_RegexFilterBulletinText(
tr::now
//,
// lt_link,
// Ui::Text::Link(
// Ui::Text::Bold(
// tr::ayu_RegexFilterBulletinAction(tr::now))),
// Ui::Text::WithEntities
),
.filter = onClick,
.adaptive = true
});
}
});
});
};
name->submits() | rpl::start_with_next(saveAndClose, name->lifetime());
box->addButton(tr::lng_settings_save(), saveAndClose);
box->addButton(tr::lng_cancel(), [=]
{ box->closeBox(); });
}
object_ptr<Ui::GenericBox> RegexEditBox(RegexFilter *filter,
const Fn<void(RegexFilter)> &onDone,
std::optional<long long> dialogId,
bool showToast)
{
return Box(RegexEditBuilder, filter, onDone, dialogId, showToast);
}
void AyuEditFilters::setupContent(not_null<Window::SessionController *> controller, QWidget *parent)
{
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
setupSettings(content, parent);
ResizeFitChild(this, content);
}
} // namespace Settings

View file

@ -0,0 +1,43 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#pragma once
#include "ayu/data/entities.h"
#include "settings/settings_common_session.h"
#include "ui/wrap/vertical_layout.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "boxes/premium_limits_box.h"
#include "info/profile/info_profile_values.h"
#include "window/window_session_controller.h"
#include "base/unixtime.h"
class BoxContent;
namespace Window {
class Controller;
class SessionController;
} // namespace Window
namespace Settings {
class AyuEditFilters : public Section<AyuEditFilters>
{
public:
AyuEditFilters(QWidget *parent, not_null<Window::SessionController*> controller);
void setupSettings(not_null<Ui::VerticalLayout*> container, QWidget *parent);
[[nodiscard]] rpl::producer<QString> title() override;
private:
void setupContent(not_null<Window::SessionController*> controller, QWidget *parent);
RegexFilter currentFilter;
};
object_ptr<Ui::GenericBox> RegexEditBox(RegexFilter* filter, const Fn<void(RegexFilter)> &onDone, std::optional<long long> dialogId = std::nullopt, bool showToast = false);
} // namespace Settings

View file

@ -0,0 +1,169 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#include "peer_global_exclusion.h"
#include "data/data_peer.h"
#include "main/main_session.h"
#include "styles/style_boxes.h"
#include "ui/painter.h"
#include <lang_auto.h>
#include <utility>
#include "settings_filters_list.h"
#include "ayu/data/ayu_database.h"
#include "ayu/utils/telegram_helpers.h"
#include "data/data_session.h"
#include "window/window_session_controller.h"
namespace Settings {
GlobalExclusionListRow::GlobalExclusionListRow(PeerId peer)
: PeerListRow(peer.value)
, peerId(peer)
{
}
QString GlobalExclusionListRow::generateName() {
if (const auto peerCached = currentSession()->data().peerLoaded(peerId)) {
this->setPeer(peerCached);
return PeerListRow::generateName();
}
return QString("UNKNOWN (ID: %1)").arg(QString::number(peerId.value & PeerId::kChatTypeMask));
}
PaintRoundImageCallback GlobalExclusionListRow::generatePaintUserpicCallback(bool forceRound) {
if (const auto peerCached = currentSession()->data().peerLoaded(peerId)) {
this->setPeer(peerCached);
return PeerListRow::generatePaintUserpicCallback(forceRound);
}
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
using namespace Ui;
const auto realId = peerId.value & PeerId::kChatTypeMask;
auto _userpicEmpty = std::make_unique<EmptyUserpic>(
EmptyUserpic::UserpicColor(realId % 7),
QString("U")); // U - Unknown
_userpicEmpty->paintCircle(p, x, y, outerWidth, size);
};
}
GlobalExclusionListController::GlobalExclusionListController(not_null<Main::Session *> session,
not_null<Window::SessionController*> controller)
: _session(session)
, _controller(controller)
{
}
Main::Session &GlobalExclusionListController::session() const {
return *_session;
}
void GlobalExclusionListController::prepare() {
const auto filters = AyuDatabase::getAllRegexFilters();
const auto exclusions = AyuDatabase::getAllFiltersExclusions();
if (exclusions.empty()) {
auto description = object_ptr<Ui::FlatLabel>(
nullptr,
tr::ayu_RegexFiltersListEmpty(tr::now),
computeListSt().about);
delegate()->peerListSetDescription(std::move(description));
return;
}
struct FilterCounts {
int filters = 0;
int exclusions = 0;
};
std::unordered_map<ID, FilterCounts> countsByDialogIds;
for (const auto &filter : filters) {
if (filter.dialogId.has_value()) {
countsByDialogIds[filter.dialogId.value()].filters++;
}
}
for (const auto &exclusion : exclusions) {
countsByDialogIds[exclusion.dialogId].exclusions++;
}
for (const auto &[id, count] : countsByDialogIds) {
PeerId peerId = PeerId(PeerIdHelper(id));
auto row = std::make_unique<GlobalExclusionListRow> (peerId);
row->setCustomStatus(QString("%1 filters, %2 excluded").arg(count.filters).arg(count.exclusions), false);
delegate()->peerListAppendRow(reinterpret_cast<std::unique_ptr<PeerListRow> &&>(row));
}
// sortByName();
delegate()->peerListRefreshRows();
}
void GlobalExclusionListController::rowClicked(not_null<PeerListRow*> peer) {
_controller->dialogId = peer->id() & PeerId::kChatTypeMask;
_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());
}
}

View file

@ -0,0 +1,71 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#pragma once
#include "boxes/peer_list_box.h"
#include "boxes/peer_list_controllers.h"
#include "data/data_peer.h"
#include "history/history.h"
class RegexFilterGlobalExclusion;
namespace Main {
class Session;
} // namespace Main
namespace Settings {
class GlobalExclusionListRow final : public PeerListRow
{
public:
explicit GlobalExclusionListRow(PeerId peer);
QString generateName() override;
PaintRoundImageCallback generatePaintUserpicCallback(bool forceRound) override;
private:
PeerId peerId;
};
class GlobalExclusionListController final : public PeerListController
{
public:
explicit GlobalExclusionListController(not_null<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:
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

View file

@ -0,0 +1,276 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#include "settings_filters_list.h"
#include <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 "rpl/mappers.h"
#include "ui/qt_object_factory.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
namespace Settings
{
rpl::producer<QString> AyuFiltersList::title()
{
return tr::ayu_RegexFilters();
}
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::addNewFilter(const RegexFilter &filter, bool exclusion)
{
// stolen from EditPrivacyBox
auto state = lifetime().make_state<RegexFilter>(filter);
auto buttonText = lifetime().make_state<rpl::variable<QString>>(
QString::fromStdString(state->text));
auto isRemoved = lifetime().make_state<rpl::variable<bool>>(false);
auto wrap = _content->add(
object_ptr<Ui::SlideWrap<Button>>(
_content,
object_ptr<Button>(
_content,
buttonText->value(),
st::settingsButtonNoIcon
)
)
);
const auto button = wrap->entity();
wrap->toggleOn(isRemoved->value() | rpl::map(!rpl::mappers::_1));
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, [=](const RegexFilter &done) mutable
{
buttonText->force_assign(QString::fromStdString(done.text));
*state = done;
}));
}, &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();
if (!state->enabled) {
button->setColorOverride(st::storiesComposeGrayText->c);
} else {
button->setColorOverride({});
}
button->update();
}, state->enabled ? &st::menuIconBlock : &st::menuIconUnblock);
_contextMenu->addSeparator();
_contextMenu->addAction(tr::lng_theme_delete(tr::now), [=, this]
{
AyuDatabase::deleteFilter(state->id);
AyuDatabase::deleteExclusionsByFilterId(state->id);
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
isRemoved->force_assign(true);
this->update();
updateGeometry();
repaint();
// remove headers if there are no more filters left
if (filters.empty() && filtersTitle) {
filtersTitle->hide();
}
if (exclusions.empty() && excludedTitle) {
excludedTitle->hide();
}
// resize();
// update();
}, &st::menuIconDelete);
_contextMenu->popup(QCursor::pos());
};
// we've opened filters list from top "Exclude" button
// on click, close the section
auto exclusionsClickHandler = [=, this]() mutable {
Expects(dialogId.has_value());
RegexFilterGlobalExclusion exclusion;
exclusion.filterId = state->id;
exclusion.dialogId = dialogId.value();
AyuDatabase::addRegexExclusion(exclusion);
FiltersCacheController::rebuildCache();
AyuSettings::fire_filtersUpdate();
_controller->showExclude = true;
_controller->dialogId = dialogId;
_controller->showSettings(AyuFiltersList::Id());
};
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();
isRemoved->force_assign(true);
this->update();
updateGeometry();
repaint();
// remove headers if there are no more filters left
if (filters.empty() && filtersTitle) {
filtersTitle->hide();
}
if (exclusions.empty() && excludedTitle) {
excludedTitle->hide();
}
}, &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()) {
filtersTitle = AddSubsectionTitle(container, tr::ayu_RegexFiltersHeader());
for (const auto &filter : filters) {
addNewFilter(filter);
}
}
if (!exclusions.empty()) {
excludedTitle = AddSubsectionTitle(container, tr::ayu_RegexFiltersExcluded());
for (const auto &exclusion : exclusions) {
addNewFilter(exclusion, true);
}
}
}
void AyuFiltersList::setupContent(not_null<Window::SessionController *> controller)
{
initializeSharedFilters(_content);
ResizeFitChild(this, _content);
}
} // namespace Settings

View file

@ -0,0 +1,52 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2025
#pragma once
#include "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;
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

View file

@ -9,6 +9,10 @@
#include "lang_auto.h"
#include "settings_ayu_utils.h"
#include "ayu/ayu_settings.h"
#include "ayu/features/filters/filters_cache_controller.h"
#include "boxes/peer_list_box.h"
#include "filters/peer_global_exclusion.h"
#include "filters/settings_filters_list.h"
#include "settings/settings_common.h"
#include "styles/style_settings.h"
#include "ui/vertical_list.h"
@ -242,6 +246,116 @@ void SetupSpyEssentials(not_null<Ui::VerticalLayout*> container) {
container->lifetime());
}
void SetupFiltersSettings(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
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();
},
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();
},
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();
},
container->lifetime());
}
void SetupShared(not_null<Window::SessionController *> controller,
Ui::VerticalLayout *container) {
Ui::AddSkip(container);
auto button = container->add(object_ptr<Ui::SettingsButton>(
container,
rpl::single(QString("Shared Filters"))
));
button->addClickHandler([=] {
controller->dialogId = std::nullopt; // ensure we're handling shared filters
controller->showExclude = false;
controller->showSettings(AyuFiltersList::Id());
});
}
void SetupExclusions(
not_null<Window::SessionController*> controller,
not_null<Ui::VerticalLayout*> container
) {
container->add(object_ptr<Ui::SettingsButton>(
container,
rpl::single(QString("Exclusions"))
))->addClickHandler([=] {
auto ctrl = std::make_unique<GlobalExclusionListController>(
&controller->session(),
controller
);
auto box = Box<PeerListBox>(std::move(ctrl), [](not_null<PeerListBox*> box) {
box->setTitle(rpl::single(QString("Exclusions")));
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
});
controller->show(std::move(box));
});
}
void SetupMessageFilters(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
@ -327,7 +441,15 @@ void AyuGhost::setupContent(not_null<Window::SessionController*> controller) {
AddDivider(content);
AddSkip(content);
SetupMessageFilters(content);
SetupFiltersSettings(content);
AddDivider(content);
SetupShared(controller, content);
SetupExclusions(controller, content);
AddSkip(content);
AddDivider(content);

View file

@ -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,44 @@ 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();
}

View file

@ -61,4 +61,7 @@ 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();

View file

@ -85,6 +85,12 @@ public:
return _peer;
}
// AyuGram
void setPeer(not_null<PeerData*> peer) {
_peer = peer;
}
// AyuGram
[[nodiscard]] PeerListRowId id() const {
return _id;
}

View file

@ -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,15 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
hasCopyRestrictionForSelected()));
}, &st::menuIconTranslate);
}
if (settings.filtersEnabled) {
_menu->addAction(tr::ayu_RegexFilterQuickAdd(tr::now), [=] {
RegexFilter filter;
filter.text = selectedText.rich.text.toStdString();
auto dialogId = static_cast<long long>(item->history()->peer->id.value & PeerId::kChatTypeMask);
_controller->show(Settings::RegexEditBox(&filter, {}, dialogId, true));
}, &st::menuIconAddToFolder);
}
addItemActions(item, item);
} else {
addReplyAction(partItemOrLeader);

View file

@ -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)) {

View file

@ -661,7 +661,11 @@ HistoryWidget::HistoryWidget(
AyuSettings::get_hideFromBlockedReactive() | rpl::to_empty,
session().changes().peerUpdates(
Data::PeerUpdate::Flag::IsBlocked
) | rpl::to_empty
) | rpl::to_empty,
AyuSettings::get_filtersUpdate() | rpl::to_empty,
AyuSettings::get_filtersEnabledReactive() | rpl::to_empty,
AyuSettings::get_filtersEnabledInChatsReactive() | rpl::to_empty,
AyuSettings::get_shadowBanIdsReactive() | rpl::to_empty
) | rpl::start_with_next(
[=]
{

View file

@ -1592,7 +1592,7 @@ void Element::destroyUnreadBar() {
}
int Element::displayedDateHeight() const {
if (AyuFeatures::MessageShot::isTakingShot()) {
if (AyuFeatures::MessageShot::isTakingShot() || isMessageHidden(data())) {
return 0;
}

View file

@ -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 = [&](

View file

@ -53,7 +53,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_menu_icons.h"
#include "styles/style_layers.h"
// AyuGram includes
#include <styles/style_ayu_icons.h>
#include <styles/style_settings.h>
#include "ayu/ui/settings/settings_ayu.h"
#include "ayu/ui/settings/filters/edit_filter.h"
#include "ayu/ui/settings/filters/settings_filters_list.h"
#include "info/info_memento.h"
namespace Info {
namespace {
@ -410,6 +417,58 @@ void WrapWidget::setupTopBarMenuToggle() {
Box(Ui::FillPeerQrBox, self, std::nullopt, nullptr));
});
}
} else if (section.settingsType() == ::Settings::AyuFiltersList::Id()) {
const auto controller = _controller->parentController();
if (true) {
const auto &st = st::ayuFiltersAddIcon;
const auto button = _topBar->addButton(
base::make_unique_q<Ui::IconButton>(_topBar, st));
const auto show = controller->uiShow();
button->addClickHandler([=, content = _content.data()]
{
const auto onDone = [=](const RegexFilter &)
{
// taken from WrapWidget::createMemento
// I cant neither update nor redraw filters list (skill issue)
// so just close current section and
// open new with the same memento
auto contentMemento = content->createMemento();
if (!contentMemento) {
return;
}
std::vector<std::shared_ptr<ContentMemento>> stack;
stack.push_back(std::move(contentMemento));
auto sectionMemento = std::make_shared<Memento>(std::move(stack));
// close the last section
showBackFromStackInternal(Window::SectionShow(Window::SectionShow::Way::Backward, anim::type::normal));
// open new section
showInternal(gsl::make_not_null(sectionMemento.get()), Window::SectionShow(anim::type::normal));
};
show->show(::Settings::RegexEditBox(nullptr, onDone, controller->dialogId));
});
}
if (controller->showExclude.has_value() && controller->showExclude.value()) {
auto icon = base::make_unique_q<Ui::IconButton>(_topBar, st::ayuFiltersExcludeIcon);
const auto excludeButton = _topBar->addButton(std::move(icon));
excludeButton->addClickHandler([=, content = _content.data()]
{
// close current filters list
showBackFromStackInternal(Window::SectionShow(Window::SectionShow::Way::Backward, anim::type::normal));
// open new
controller->showExclude = false;
controller->showSettings(::Settings::AyuFiltersList::Id());
});
}
}
} else if (key.storiesPeer()
&& key.storiesPeer()->isSelf()

View file

@ -1579,6 +1579,7 @@ void Filler::fillProfileActions() {
addManageTopic();
addToggleTopicClosed();
AyuUi::AddOpenChannelAction(_peer, _controller, _addAction);
AyuUi::AddShadowBanAction(_peer, _addAction);
addViewDiscussion();
addDirectMessages();
addExportChat();

View file

@ -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;