mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-07-25 06:54:43 +00:00
chore: rework filters & fix crashes
This commit is contained in:
@@ -1051,9 +1051,9 @@ void AyuSettings::setSingleCornerRadius(bool val) {
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const AyuSettings &s) {
|
||||
std::map<std::string, GhostModeAccountSettings> ghostAccounts;
|
||||
auto ghostAccounts = nlohmann::json::object();
|
||||
for (const auto &[key, value] : s._ghostAccounts) {
|
||||
ghostAccounts[std::to_string(key)] = std::move(*value);
|
||||
ghostAccounts[std::to_string(key)] = *value;
|
||||
}
|
||||
|
||||
j = nlohmann::json{
|
||||
|
||||
@@ -248,6 +248,10 @@ void addEditedMessage(const EditedMessage &message) {
|
||||
storage.insert(message);
|
||||
storage.commit();
|
||||
} catch (std::exception &ex) {
|
||||
try {
|
||||
storage.rollback();
|
||||
} catch (...) {
|
||||
}
|
||||
LOG(("Failed to save edited message for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
@@ -289,6 +293,10 @@ void addDeletedMessage(const DeletedMessage &message) {
|
||||
storage.insert(message);
|
||||
storage.commit();
|
||||
} catch (std::exception &ex) {
|
||||
try {
|
||||
storage.rollback();
|
||||
} catch (...) {
|
||||
}
|
||||
LOG(("Failed to save edited message for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
@@ -442,7 +450,10 @@ void addRegexFilter(const RegexFilter &filter) {
|
||||
storage.replace(filter); // we're using replace as we set std::vector<char> as primary key
|
||||
storage.commit();
|
||||
} catch (std::exception &ex) {
|
||||
storage.rollback();
|
||||
try {
|
||||
storage.rollback();
|
||||
} catch (...) {
|
||||
}
|
||||
LOG(("Failed to save regex filter for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
@@ -453,6 +464,10 @@ void addRegexExclusion(const RegexFilterGlobalExclusion &exclusion) {
|
||||
storage.insert(exclusion);
|
||||
storage.commit();
|
||||
} catch (std::exception &ex) {
|
||||
try {
|
||||
storage.rollback();
|
||||
} catch (...) {
|
||||
}
|
||||
LOG(("Failed to save regex filter exclusion for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#define ID long long
|
||||
using ID = long long;
|
||||
|
||||
class AyuMessageBase
|
||||
{
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
#include "history/history_item.h"
|
||||
#include "rpl/event_stream.h"
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_set>
|
||||
|
||||
static std::mutex mutex;
|
||||
|
||||
namespace FiltersCacheController {
|
||||
|
||||
std::mutex rebuildMutex;
|
||||
std::mutex cacheMutex;
|
||||
std::mutex filteredMessagesMutex;
|
||||
|
||||
rpl::event_stream<> filtersUpdateStream;
|
||||
|
||||
void fireUpdate() {
|
||||
@@ -31,21 +35,17 @@ rpl::producer<> updates() {
|
||||
return filtersUpdateStream.events();
|
||||
}
|
||||
|
||||
std::optional<std::vector<HashablePattern>> sharedPatterns;
|
||||
std::optional<std::unordered_map<long long, std::vector<ReversiblePattern>>> patternsByDialogId;
|
||||
std::shared_ptr<const Cache> cache;
|
||||
|
||||
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);
|
||||
std::unordered_map<long long, std::unordered_map<int64, bool>> filteredMessages;
|
||||
std::unordered_set<BareId> dialogsWithHiddenBlockedMessages; // purely for show / hide filtered messages
|
||||
|
||||
std::shared_ptr<const Cache> buildCache() {
|
||||
const auto filters = AyuDatabase::getAllRegexFilters();
|
||||
const auto exclusions = AyuDatabase::getAllFiltersExclusions();
|
||||
|
||||
std::vector<HashablePattern> shared;
|
||||
std::unordered_map<long long, std::vector<ReversiblePattern>> byDialogId;
|
||||
std::unordered_map<ID, std::vector<ReversiblePattern>> byDialogId;
|
||||
|
||||
for (const auto &filter : filters) {
|
||||
if (!filter.enabled || filter.text.empty()) {
|
||||
@@ -56,25 +56,47 @@ void rebuildCache() {
|
||||
if (filter.caseInsensitive) flags |= UREGEX_CASE_INSENSITIVE;
|
||||
|
||||
auto status = U_ZERO_ERROR;
|
||||
auto pattern = RegexPattern::compile(UnicodeString::fromUTF8(filter.text), flags, status);
|
||||
auto pattern = icu::RegexPattern::compile(icu::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});
|
||||
byDialogId[filter.dialogId.value()].push_back({
|
||||
std::shared_ptr<icu::RegexPattern>(pattern),
|
||||
filter.reversed,
|
||||
});
|
||||
} else {
|
||||
shared.push_back({filter.id, {std::shared_ptr<RegexPattern>(pattern), filter.reversed}});
|
||||
shared.push_back({
|
||||
filter.id,
|
||||
{
|
||||
std::shared_ptr<icu::RegexPattern>(pattern),
|
||||
filter.reversed,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
auto exclByDialogId = buildExclusions(exclusions, shared);
|
||||
auto result = std::make_shared<Cache>();
|
||||
result->sharedPatterns = std::move(shared);
|
||||
result->patternsByDialogId = std::move(byDialogId);
|
||||
result->exclusionsByDialogId = std::move(exclByDialogId);
|
||||
|
||||
sharedPatterns = shared;
|
||||
patternsByDialogId = byDialogId;
|
||||
exclusionsByDialogId = exclByDialogId;
|
||||
filteredMessages.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
void rebuildCache() {
|
||||
std::lock_guard rebuildLock(rebuildMutex);
|
||||
auto next = buildCache();
|
||||
{
|
||||
std::lock_guard cacheLock(cacheMutex);
|
||||
std::lock_guard filteredLock(filteredMessagesMutex);
|
||||
cache = std::move(next);
|
||||
filteredMessages.clear();
|
||||
dialogsWithHiddenBlockedMessages.clear();
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<long long, std::unordered_set<HashablePattern, PatternHasher>> buildExclusions(
|
||||
@@ -95,8 +117,32 @@ std::unordered_map<long long, std::unordered_set<HashablePattern, PatternHasher>
|
||||
return exclusionsByDialogId;
|
||||
}
|
||||
|
||||
std::shared_ptr<const Cache> snapshot() {
|
||||
{
|
||||
std::lock_guard lock(cacheMutex);
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard rebuildLock(rebuildMutex);
|
||||
{
|
||||
std::lock_guard lock(cacheMutex);
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
|
||||
auto next = buildCache();
|
||||
std::lock_guard lock(cacheMutex);
|
||||
if (!cache) {
|
||||
cache = std::move(next);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
std::optional<bool> isFiltered(not_null<HistoryItem*> item) {
|
||||
std::lock_guard lock(mutex);
|
||||
std::lock_guard lock(filteredMessagesMutex);
|
||||
auto dialogIt = filteredMessages.find(item->history()->peer->id.value);
|
||||
|
||||
if (dialogIt == filteredMessages.end()) {
|
||||
@@ -112,21 +158,38 @@ std::optional<bool> isFiltered(not_null<HistoryItem*> item) {
|
||||
}
|
||||
|
||||
bool hasFilteredMessages(not_null<PeerData*> peer) {
|
||||
std::lock_guard lock(mutex);
|
||||
std::lock_guard lock(filteredMessagesMutex);
|
||||
if (dialogsWithHiddenBlockedMessages.contains(peer->id.value)) {
|
||||
return true;
|
||||
}
|
||||
const auto dialogIt = filteredMessages.find(peer->id.value);
|
||||
if (dialogIt == filteredMessages.end()) {
|
||||
return false;
|
||||
}
|
||||
for (const auto &entry : dialogIt->second) {
|
||||
if (entry.second == true) {
|
||||
if (entry.second) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void putFiltered(not_null<HistoryItem*> item, const Data::Group *group, bool res) {
|
||||
std::lock_guard lock(mutex);
|
||||
void putHiddenBlockedMessage(not_null<HistoryItem*> item) {
|
||||
std::lock_guard lock(filteredMessagesMutex);
|
||||
dialogsWithHiddenBlockedMessages.insert(item->history()->peer->id.value);
|
||||
}
|
||||
|
||||
void putFiltered(
|
||||
not_null<HistoryItem*> item,
|
||||
const Data::Group *group,
|
||||
bool res,
|
||||
const std::shared_ptr<const Cache> &matchedCache) {
|
||||
std::lock_guard cacheLock(cacheMutex);
|
||||
if (cache != matchedCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard filteredLock(filteredMessagesMutex);
|
||||
filteredMessages[item->history()->peer->id.value][item->id.bare] = res;
|
||||
if (group && res) {
|
||||
for (const auto& groupItem : group->items) {
|
||||
@@ -151,7 +214,7 @@ void invalidateSingle(not_null<HistoryItem*> item) {
|
||||
}
|
||||
|
||||
void invalidate(not_null<HistoryItem*> item) {
|
||||
std::lock_guard lock(mutex);
|
||||
std::lock_guard lock(filteredMessagesMutex);
|
||||
if (const auto group = item->history()->owner().groups().find(item)) {
|
||||
for (const auto& groupItem : group->items) {
|
||||
invalidateSingle(groupItem);
|
||||
@@ -161,36 +224,4 @@ void invalidate(not_null<HistoryItem*> item) {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,31 +10,47 @@
|
||||
#include "ayu/features/filters/filters_controller.h"
|
||||
#include "rpl/producer.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Data {
|
||||
struct Group;
|
||||
}
|
||||
|
||||
using namespace FiltersController;
|
||||
|
||||
namespace FiltersCacheController {
|
||||
|
||||
using HashablePattern = FiltersController::HashablePattern;
|
||||
using PatternHasher = FiltersController::PatternHasher;
|
||||
using ReversiblePattern = FiltersController::ReversiblePattern;
|
||||
|
||||
void fireUpdate();
|
||||
[[nodiscard]] rpl::producer<> updates();
|
||||
|
||||
void rebuildCache();
|
||||
|
||||
struct Cache
|
||||
{
|
||||
std::vector<HashablePattern> sharedPatterns;
|
||||
std::unordered_map<ID, std::vector<ReversiblePattern>> patternsByDialogId;
|
||||
std::unordered_map<ID, std::unordered_set<HashablePattern, PatternHasher>> exclusionsByDialogId;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::shared_ptr<const Cache> snapshot();
|
||||
|
||||
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);
|
||||
bool hasFilteredMessages(not_null<PeerData*> peer);
|
||||
void putFiltered(not_null<HistoryItem*> item, const Data::Group *group, bool res);
|
||||
void putHiddenBlockedMessage(not_null<HistoryItem*> item);
|
||||
void putFiltered(
|
||||
not_null<HistoryItem*> item,
|
||||
const Data::Group *group,
|
||||
bool res,
|
||||
const std::shared_ptr<const Cache> &matchedCache);
|
||||
|
||||
void invalidate(not_null<HistoryItem*> item);
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "history/history_item_components.h"
|
||||
#include "unicode/regex.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace FiltersController {
|
||||
@@ -35,23 +36,27 @@ bool filterBlocked(const not_null<HistoryItem*> item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<bool> isFiltered(const QString &str, uint64 dialogId) {
|
||||
std::optional<bool> isFiltered(
|
||||
const QString &str,
|
||||
long long dialogId,
|
||||
const std::shared_ptr<const FiltersCacheController::Cache> &cache) {
|
||||
if (str.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto icuStr = UnicodeString(reinterpret_cast<const UChar*>(str.constData()), str.length());
|
||||
const auto icuStr = icu::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)) {
|
||||
const auto matcher = std::unique_ptr<icu::RegexMatcher>(pattern.pattern->matcher(icuStr, status));
|
||||
if (U_FAILURE(status) || !matcher) {
|
||||
LOG(("FILTER FAILED: %1").arg(u_errorName(status)));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto match = matcher->find();
|
||||
const auto reversed = pattern.reversed;
|
||||
|
||||
if ((!reversed && match) || (reversed && !match)) {
|
||||
@@ -60,20 +65,18 @@ std::optional<bool> isFiltered(const QString &str, uint64 dialogId) {
|
||||
return false;
|
||||
};
|
||||
|
||||
const auto &dialogPatterns = FiltersCacheController::getPatternsByDialogId(dialogId);
|
||||
if (dialogPatterns.has_value() && !dialogPatterns.value().empty()) {
|
||||
for (const auto &pattern : dialogPatterns.value()) {
|
||||
if (const auto i = cache->patternsByDialogId.find(dialogId); i != cache->patternsByDialogId.end()) {
|
||||
for (const auto &pattern : i->second) {
|
||||
if (matches(pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto &exclusions = FiltersCacheController::getExclusionsByDialogId(dialogId);
|
||||
const auto &sharedPatterns = FiltersCacheController::getSharedPatterns();
|
||||
if (!sharedPatterns.empty()) {
|
||||
for (const auto &pattern : sharedPatterns) {
|
||||
if (exclusions.has_value() && exclusions.value().contains(pattern)) {
|
||||
const auto exclusions = cache->exclusionsByDialogId.find(dialogId);
|
||||
if (!cache->sharedPatterns.empty()) {
|
||||
for (const auto &pattern : cache->sharedPatterns) {
|
||||
if (exclusions != cache->exclusionsByDialogId.end() && exclusions->second.contains(pattern)) {
|
||||
continue;
|
||||
}
|
||||
if (matches(pattern.pattern)) {
|
||||
@@ -92,29 +95,44 @@ bool isEnabled(not_null<PeerData*> peer) {
|
||||
bool isBlocked(const not_null<HistoryItem*> item) {
|
||||
const auto &settings = AyuSettings::getInstance();
|
||||
|
||||
auto shadowBanMatched = false;
|
||||
const auto blocked = [&]() -> bool
|
||||
{
|
||||
if (item->from()->isUser() &&
|
||||
item->from()->asUser()->isBlocked()) {
|
||||
const auto isShadowBanned = [&](PeerData *peer) {
|
||||
return peer
|
||||
&& (peer->isUser() || peer->isBroadcast())
|
||||
&& settings.isShadowBanned(getDialogIdFromPeer(peer));
|
||||
};
|
||||
|
||||
if (isShadowBanned(item->from())
|
||||
&& item->from()->id != item->history()->peer->id) {
|
||||
shadowBanMatched = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
if (const auto originalSender = forwarded->originalSender) {
|
||||
const auto originalShadowBanned = isShadowBanned(originalSender);
|
||||
if (originalShadowBanned
|
||||
|| (originalSender->isUser()
|
||||
&& originalSender->asUser()->isBlocked())) {
|
||||
shadowBanMatched = originalShadowBanned;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
|
||||
return settings.filtersEnabled() &&
|
||||
(
|
||||
((item->from()->isUser() || item->from()->isBroadcast()) && settings.isShadowBanned(getDialogIdFromPeer(item->from()))) ||
|
||||
(settings.hideFromBlocked() && blocked)
|
||||
);
|
||||
return settings.filtersEnabled()
|
||||
&& (shadowBanMatched || settings.hideFromBlocked())
|
||||
&& blocked;
|
||||
}
|
||||
|
||||
bool isBlocked(const not_null<PeerData*> peer) {
|
||||
@@ -140,7 +158,10 @@ bool filtered(const not_null<HistoryItem*> item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterBlocked(item)) return true;
|
||||
if (filterBlocked(item)) {
|
||||
FiltersCacheController::putHiddenBlockedMessage(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isEnabled(item->history()->peer)) return false;
|
||||
|
||||
@@ -149,13 +170,17 @@ bool filtered(const not_null<HistoryItem*> item) {
|
||||
return cached.value();
|
||||
}
|
||||
const auto group = item->history()->owner().groups().find(item);
|
||||
const auto res = isFiltered(FilterUtils::extractAllText(item, group), getDialogIdFromPeer(item->history()->peer));
|
||||
const auto cache = FiltersCacheController::snapshot();
|
||||
const auto res = isFiltered(
|
||||
FilterUtils::extractAllText(item, group),
|
||||
getDialogIdFromPeer(item->history()->peer),
|
||||
cache);
|
||||
|
||||
// sometimes item has empty text.
|
||||
// so we cache result only if
|
||||
// processed item is filterable
|
||||
if (res.has_value()) {
|
||||
FiltersCacheController::putFiltered(item, group, res.value());
|
||||
FiltersCacheController::putFiltered(item, group, res.value(), cache);
|
||||
return res.value();
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -8,15 +8,14 @@
|
||||
|
||||
#include "unicode/regex.h"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace icu_78;
|
||||
|
||||
namespace FiltersController {
|
||||
|
||||
bool isEnabled(PeerData *peer);
|
||||
bool isEnabled(not_null<PeerData*> peer);
|
||||
bool isBlocked(not_null<HistoryItem*> item);
|
||||
bool isBlocked(not_null<PeerData*> peer);
|
||||
bool filtered(not_null<HistoryItem*> historyItem);
|
||||
@@ -27,7 +26,7 @@ void invalidate(not_null<HistoryItem*> item);
|
||||
|
||||
struct ReversiblePattern
|
||||
{
|
||||
std::shared_ptr<RegexPattern> pattern;
|
||||
std::shared_ptr<icu::RegexPattern> pattern;
|
||||
bool reversed;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,27 +6,41 @@
|
||||
// Copyright @Radolyn, 2026
|
||||
#include "ayu/features/filters/filters_utils.h"
|
||||
|
||||
#include "apiwrap.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/utils/telegram_helpers.h"
|
||||
#include "base/qthelp_url.h"
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "core/local_url_handlers.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "lang/lang_text_entity.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_domain.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/toast/toast.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <QByteArray>
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonArray>
|
||||
#include <qjsondocument.h>
|
||||
#include <QString>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <QtNetwork/QHttpPart>
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
@@ -34,6 +48,247 @@
|
||||
|
||||
constexpr auto BACKUP_VERSION = 2;
|
||||
|
||||
enum class PeerResolveHintType {
|
||||
Username,
|
||||
Invite,
|
||||
};
|
||||
|
||||
struct PeerResolveHint {
|
||||
PeerResolveHintType type = PeerResolveHintType::Username;
|
||||
QString value;
|
||||
};
|
||||
|
||||
bool HasChanges(const ApplyChanges &changes) {
|
||||
return !changes.newFilters.empty()
|
||||
|| !changes.removeFiltersById.empty()
|
||||
|| !changes.filtersOverrides.empty()
|
||||
|| !changes.newExclusions.empty()
|
||||
|| !changes.removeExclusions.empty()
|
||||
|| !changes.peersToBeResolved.empty();
|
||||
}
|
||||
|
||||
void AppendChangeSummary(
|
||||
TextWithEntities &summary,
|
||||
TextWithEntities &&line) {
|
||||
if (!summary.text.isEmpty()) {
|
||||
summary.append('\n');
|
||||
}
|
||||
summary.append(std::move(line));
|
||||
}
|
||||
|
||||
TextWithEntities ChangeSummaryText(const ApplyChanges &changes) {
|
||||
auto result = tr::marked();
|
||||
|
||||
if (!changes.newFilters.empty()) {
|
||||
AppendChangeSummary(
|
||||
result,
|
||||
tr::ayu_FiltersSheetNewFilters(
|
||||
tr::now,
|
||||
lt_count,
|
||||
int(changes.newFilters.size()),
|
||||
tr::rich));
|
||||
}
|
||||
if (!changes.removeFiltersById.empty()) {
|
||||
AppendChangeSummary(
|
||||
result,
|
||||
tr::ayu_FiltersSheetRemovedFilters(
|
||||
tr::now,
|
||||
lt_count,
|
||||
int(changes.removeFiltersById.size()),
|
||||
tr::rich));
|
||||
}
|
||||
if (!changes.filtersOverrides.empty()) {
|
||||
AppendChangeSummary(
|
||||
result,
|
||||
tr::ayu_FiltersSheetUpdatedFilters(
|
||||
tr::now,
|
||||
lt_count,
|
||||
int(changes.filtersOverrides.size()),
|
||||
tr::rich));
|
||||
}
|
||||
if (!changes.newExclusions.empty()) {
|
||||
AppendChangeSummary(
|
||||
result,
|
||||
tr::ayu_FiltersSheetNewExclusions(
|
||||
tr::now,
|
||||
lt_count,
|
||||
int(changes.newExclusions.size()),
|
||||
tr::rich));
|
||||
}
|
||||
if (!changes.removeExclusions.empty()) {
|
||||
AppendChangeSummary(
|
||||
result,
|
||||
tr::ayu_FiltersSheetRemovedExclusions(
|
||||
tr::now,
|
||||
lt_count,
|
||||
int(changes.removeExclusions.size()),
|
||||
tr::rich));
|
||||
}
|
||||
if (!changes.peersToBeResolved.empty()) {
|
||||
AppendChangeSummary(
|
||||
result,
|
||||
tr::ayu_FiltersSheetDialogsToResolve(
|
||||
tr::now,
|
||||
lt_count,
|
||||
int(changes.peersToBeResolved.size()),
|
||||
tr::rich));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<PeerResolveHint> ParsePeerResolveHint(QString value) {
|
||||
value = value.trimmed();
|
||||
if (value.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto converted = Core::TryConvertUrlToLocal(value);
|
||||
const auto local = converted.isEmpty() ? value : converted;
|
||||
const auto delimiter = local.indexOf('?');
|
||||
const auto params = (delimiter > 0)
|
||||
? qthelp::url_parse_params(
|
||||
local.mid(delimiter + 1),
|
||||
qthelp::UrlParamNameTransform::ToLower)
|
||||
: QMap<QString, QString>();
|
||||
|
||||
if (local.startsWith(u"tg://join"_q, Qt::CaseInsensitive)) {
|
||||
const auto invite = params.value(u"invite"_q);
|
||||
if (!invite.isEmpty()) {
|
||||
return PeerResolveHint{
|
||||
.type = PeerResolveHintType::Invite,
|
||||
.value = invite,
|
||||
};
|
||||
}
|
||||
} else if (local.startsWith(u"tg://resolve"_q, Qt::CaseInsensitive)) {
|
||||
const auto domain = params.value(u"domain"_q);
|
||||
if (!domain.isEmpty()) {
|
||||
return PeerResolveHint{
|
||||
.type = PeerResolveHintType::Username,
|
||||
.value = domain,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (value.startsWith('@')) {
|
||||
value = value.mid(1);
|
||||
}
|
||||
if (value.startsWith('+')) {
|
||||
return PeerResolveHint{
|
||||
.type = PeerResolveHintType::Invite,
|
||||
.value = value.mid(1),
|
||||
};
|
||||
}
|
||||
if (value.startsWith("joinchat/"_q, Qt::CaseInsensitive)) {
|
||||
return PeerResolveHint{
|
||||
.type = PeerResolveHintType::Invite,
|
||||
.value = value.mid(9),
|
||||
};
|
||||
}
|
||||
|
||||
const auto slash = value.indexOf('/');
|
||||
if (slash >= 0) {
|
||||
value = value.left(slash);
|
||||
}
|
||||
if (value.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return PeerResolveHint{
|
||||
.type = PeerResolveHintType::Username,
|
||||
.value = value,
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<char> ParseFilterId(QString id) {
|
||||
id.remove('-');
|
||||
if (id.size() != 32) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto bytes = QByteArray::fromHex(id.toUtf8());
|
||||
if (bytes.size() != 16) {
|
||||
return {};
|
||||
}
|
||||
return std::vector(bytes.constData(), bytes.constData() + bytes.size());
|
||||
}
|
||||
|
||||
PeerData *LoadedPeerFromDialogId(not_null<Data::Session*> data, ID dialogId) {
|
||||
if (!dialogId) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto bare = (dialogId < 0) ? -dialogId : dialogId;
|
||||
if (dialogId > 0) {
|
||||
return data->userLoaded(UserId(bare));
|
||||
}
|
||||
if (const auto channel = data->channelLoaded(ChannelId(bare))) {
|
||||
return channel;
|
||||
}
|
||||
return data->chatLoaded(ChatId(bare));
|
||||
}
|
||||
|
||||
void ResolveFilterBackupPeers(const std::vector<QString> &peerHints) {
|
||||
auto session = currentSession();
|
||||
auto hintsCopy = peerHints;
|
||||
|
||||
crl::async([=]
|
||||
{
|
||||
for (const auto &entry : hintsCopy) {
|
||||
const auto hint = ParsePeerResolveHint(entry);
|
||||
if (!hint || hint->value.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
auto latch = std::make_shared<TimedCountDownLatch>(1);
|
||||
auto floodWait = std::make_shared<std::atomic_bool>(false);
|
||||
|
||||
auto onFail = [=](const MTP::Error &error)
|
||||
{
|
||||
if (MTP::IsFloodError(error.type())) {
|
||||
floodWait->store(true);
|
||||
}
|
||||
latch->countDown();
|
||||
};
|
||||
|
||||
crl::on_main([=]
|
||||
{
|
||||
if (hint->type == PeerResolveHintType::Username) {
|
||||
session->api().request(MTPcontacts_ResolveUsername(
|
||||
MTP_flags(0),
|
||||
MTP_string(hint->value),
|
||||
MTP_string()
|
||||
)).done([=](const MTPcontacts_ResolvedPeer &result)
|
||||
{
|
||||
const auto &data = result.data();
|
||||
session->data().processUsers(data.vusers());
|
||||
session->data().processChats(data.vchats());
|
||||
latch->countDown();
|
||||
}).fail(onFail).send();
|
||||
} else {
|
||||
session->api().checkChatInvite(
|
||||
hint->value,
|
||||
[=](const MTPChatInvite &invite)
|
||||
{
|
||||
invite.match([=](const MTPDchatInvite &data)
|
||||
{
|
||||
}, [=](const MTPDchatInviteAlready &data)
|
||||
{
|
||||
session->data().processChat(data.vchat());
|
||||
}, [=](const MTPDchatInvitePeek &data)
|
||||
{
|
||||
session->data().processChat(data.vchat());
|
||||
});
|
||||
latch->countDown();
|
||||
},
|
||||
onFail);
|
||||
}
|
||||
});
|
||||
latch->await(std::chrono::seconds(20));
|
||||
if (floodWait->load()) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(20));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void FilterUtils::importFromLink(const QString &link) {
|
||||
if (link.isEmpty()) {
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailFetch(tr::now));
|
||||
@@ -41,15 +296,21 @@ void FilterUtils::importFromLink(const QString &link) {
|
||||
}
|
||||
|
||||
const auto request = QNetworkRequest(QUrl(link));
|
||||
_reply = _manager->get(request);
|
||||
const auto reply = _manager->get(request);
|
||||
const auto failed = std::make_shared<bool>(false);
|
||||
|
||||
connect(
|
||||
_reply,
|
||||
reply,
|
||||
&QNetworkReply::finished,
|
||||
this,
|
||||
[=]
|
||||
{
|
||||
const auto responseData = _reply->readAll();
|
||||
if (*failed) {
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto responseData = reply->readAll();
|
||||
|
||||
const auto jsonString = QString::fromUtf8(responseData);
|
||||
|
||||
@@ -57,25 +318,26 @@ void FilterUtils::importFromLink(const QString &link) {
|
||||
LOG(("FilterUtils: Invalid response."));
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
|
||||
|
||||
_reply->deleteLater();
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!handleResponse(jsonString.toUtf8())) {
|
||||
LOG(("FilterUtils: Error handling response."));
|
||||
}
|
||||
_reply->deleteLater();
|
||||
handleResponse(jsonString.toUtf8());
|
||||
reply->deleteLater();
|
||||
});
|
||||
|
||||
connect(
|
||||
_reply,
|
||||
reply,
|
||||
&QNetworkReply::errorOccurred,
|
||||
this,
|
||||
[=](QNetworkReply::NetworkError e)
|
||||
{
|
||||
if (*failed) {
|
||||
return;
|
||||
}
|
||||
*failed = true;
|
||||
gotFailure(e);
|
||||
|
||||
_reply->deleteLater();
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,17 +364,17 @@ void FilterUtils::publishFilters() {
|
||||
|
||||
QNetworkRequest request(QUrl("https://dpaste.com/api/v2/"));
|
||||
|
||||
_reply = _manager->post(request, multiPart);
|
||||
multiPart->setParent(_reply);
|
||||
const auto reply = _manager->post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
|
||||
connect(
|
||||
_reply,
|
||||
reply,
|
||||
&QNetworkReply::finished,
|
||||
this,
|
||||
[=]
|
||||
{
|
||||
const auto error = _reply->error();
|
||||
const auto location = _reply->header(QNetworkRequest::LocationHeader);
|
||||
const auto error = reply->error();
|
||||
const auto location = reply->header(QNetworkRequest::LocationHeader);
|
||||
|
||||
if (error == QNetworkReply::NoError && location.isValid()) {
|
||||
auto url = location.toString();
|
||||
@@ -121,15 +383,15 @@ void FilterUtils::publishFilters() {
|
||||
|
||||
Ui::Toast::Show(tr::lng_stickers_copied(tr::now));
|
||||
} else {
|
||||
LOG(("Failed to publish filters to dpaste, error: %1").arg(_reply->errorString()));
|
||||
LOG(("Failed to publish filters to dpaste, error: %1").arg(reply->errorString()));
|
||||
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailPublish(tr::now));
|
||||
}
|
||||
_reply->deleteLater();
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
bool FilterUtils::importFromJson(const QByteArray &json) {
|
||||
void FilterUtils::importFromJson(const QByteArray &json) {
|
||||
auto error = QJsonParseError{0, QJsonParseError::NoError};
|
||||
const auto document = QJsonDocument::fromJson(json, &error);
|
||||
|
||||
@@ -137,33 +399,37 @@ bool FilterUtils::importFromJson(const QByteArray &json) {
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
|
||||
LOG(("FilterUtils: Failed to parse JSON, error: %1"
|
||||
).arg(error.errorString()));
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
if (!document.isObject()) {
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
|
||||
LOG(("FilterUtils: not an object received in JSON"));
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
const auto changes = prepareChanges(document.object());
|
||||
|
||||
if (changes == ApplyChanges{}) {
|
||||
if (!HasChanges(changes)) {
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailNoChanges(tr::now));
|
||||
LOG(("FilterUtils: received empty changes"));
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
auto box = Ui::MakeConfirmBox({
|
||||
.text = ChangeSummaryText(changes),
|
||||
.confirmed = [=](Fn<void()> close) {
|
||||
close();
|
||||
try {
|
||||
applyChanges(changes);
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastSuccess(tr::now));
|
||||
} catch (...) {
|
||||
LOG(("FilterUtils: Failed to apply import changes"));
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
|
||||
}
|
||||
},
|
||||
.confirmText = tr::ayu_FiltersMenuImport(),
|
||||
.title = tr::ayu_FiltersSheetTitle(),
|
||||
});
|
||||
Ui::show(std::move(box));
|
||||
}
|
||||
|
||||
struct BackupExclusion
|
||||
@@ -248,7 +514,7 @@ QString FilterUtils::exportFilters() {
|
||||
if (!item.dialogId.has_value()) {
|
||||
continue;
|
||||
}
|
||||
if (const auto peer = session->data().peer(peerFromChat(abs(item.dialogId.value())))) {
|
||||
if (const auto peer = LoadedPeerFromDialogId(&session->data(), item.dialogId.value())) {
|
||||
if (!peer->username().isEmpty()) {
|
||||
QString key = QString::number(item.dialogId.value());
|
||||
peers[key] = peer->username();
|
||||
@@ -372,10 +638,14 @@ int typeOfMessage(const HistoryItem *item) {
|
||||
}
|
||||
|
||||
QString extractSingle(const 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) {
|
||||
const auto original = item->originalText();
|
||||
QString text(original.text);
|
||||
if (!original.entities.empty()) {
|
||||
for (const auto &entity : original.entities) {
|
||||
if (entity.type() == EntityType::Url) {
|
||||
text.append("\n");
|
||||
text.append(original.text.mid(entity.offset(), entity.length()));
|
||||
} else if (entity.type() == EntityType::CustomUrl) {
|
||||
text.append("\n");
|
||||
text.append(entity.data());
|
||||
}
|
||||
@@ -414,12 +684,12 @@ QString FilterUtils::extractAllText(const not_null<HistoryItem*> item, const Dat
|
||||
return text;
|
||||
}
|
||||
|
||||
bool FilterUtils::handleResponse(const QByteArray &response) {
|
||||
void FilterUtils::handleResponse(const QByteArray &response) {
|
||||
try {
|
||||
return importFromJson(response);
|
||||
importFromJson(response);
|
||||
} catch (...) {
|
||||
LOG(("FilterUtils: Failed to apply response"));
|
||||
return false;
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,9 +712,9 @@ ApplyChanges FilterUtils::prepareChanges(const QJsonObject &root) {
|
||||
std::vector<RegexFilter> filtersOverrides;
|
||||
std::map<std::vector<char>, RegexFilter> newFilters;
|
||||
std::vector<RegexFilterGlobalExclusion> newExclusions;
|
||||
std::vector<QString> removeFiltersById;
|
||||
std::vector<std::vector<char>> removeFiltersById;
|
||||
std::vector<RegexFilterGlobalExclusion> removeExclusions;
|
||||
std::map<long long, QString> peersToBeResolved;
|
||||
std::vector<QString> peersToBeResolved;
|
||||
|
||||
|
||||
if (const auto &filters = root.value("filters").toArray(); !filters.isEmpty()) {
|
||||
@@ -461,11 +731,10 @@ ApplyChanges FilterUtils::prepareChanges(const QJsonObject &root) {
|
||||
}
|
||||
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.id = ParseFilterId(filter.value("id").toString());
|
||||
if (regex.id.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
regex.reversed = filter.value("reversed").toBool();
|
||||
regex.text = filter.value("text").toString().toStdString();
|
||||
@@ -479,7 +748,7 @@ ApplyChanges FilterUtils::prepareChanges(const QJsonObject &root) {
|
||||
if (it != existingFilters.end()) {
|
||||
const RegexFilter &existing = *it;
|
||||
if (existing != regex) {
|
||||
filtersOverrides.push_back(existing);
|
||||
filtersOverrides.push_back(std::move(regex));
|
||||
}
|
||||
} else {
|
||||
newFilters[regex.id] = std::move(regex);
|
||||
@@ -495,14 +764,10 @@ ApplyChanges FilterUtils::prepareChanges(const QJsonObject &root) {
|
||||
|
||||
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()
|
||||
);
|
||||
regex.filterId = ParseFilterId(exclusion.value("filterId").toString());
|
||||
if (regex.filterId.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto it = std::ranges::find_if(
|
||||
existingExclusions,
|
||||
@@ -521,10 +786,10 @@ ApplyChanges FilterUtils::prepareChanges(const QJsonObject &root) {
|
||||
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 filterId = ParseFilterId(filterRef.toString());
|
||||
if (filterId.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto exists = std::ranges::any_of(
|
||||
existingFilters,
|
||||
@@ -533,7 +798,7 @@ ApplyChanges FilterUtils::prepareChanges(const QJsonObject &root) {
|
||||
return f.id == filterId;
|
||||
});
|
||||
if (exists) {
|
||||
removeFiltersById.push_back(filter);
|
||||
removeFiltersById.push_back(filterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,23 +806,24 @@ ApplyChanges FilterUtils::prepareChanges(const QJsonObject &root) {
|
||||
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 auto filterId = ParseFilterId(exclusionObj.value("filterId").toString());
|
||||
if (filterId.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool exists = std::ranges::any_of(
|
||||
existingExclusions,
|
||||
[&](const RegexFilterGlobalExclusion &x)
|
||||
{
|
||||
return x.filterId == filterIdVec && x.dialogId == dialogId;
|
||||
return x.filterId == filterId && x.dialogId == dialogId;
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
RegexFilterGlobalExclusion regex;
|
||||
regex.dialogId = dialogId;
|
||||
regex.filterId = filterIdVec;
|
||||
regex.filterId = filterId;
|
||||
removeExclusions.push_back(regex);
|
||||
}
|
||||
}
|
||||
@@ -567,24 +833,22 @@ ApplyChanges FilterUtils::prepareChanges(const QJsonObject &root) {
|
||||
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 (parsed) {
|
||||
for (const auto &[index, account] : Core::App().domain().accounts()) {
|
||||
if (const auto session = account->maybeSession()) {
|
||||
if (const auto peer = LoadedPeerFromDialogId(&session->data(), dialogId)) {
|
||||
peerMaybe = peer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!peerMaybe) {
|
||||
const auto username = peersJson.value(dialogIdStr).toString();
|
||||
peersToBeResolved[dialogId] = username;
|
||||
const auto resolverHint = peersJson.value(dialogIdStr).toString();
|
||||
peersToBeResolved.push_back(resolverHint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -611,9 +875,8 @@ void FilterUtils::applyChanges(const ApplyChanges &changes) {
|
||||
|
||||
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);
|
||||
AyuDatabase::deleteExclusionsByFilterId(id);
|
||||
AyuDatabase::deleteFilter(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,7 +899,7 @@ void FilterUtils::applyChanges(const ApplyChanges &changes) {
|
||||
}
|
||||
|
||||
if (!changes.peersToBeResolved.empty()) {
|
||||
resolveAllChats(changes.peersToBeResolved);
|
||||
ResolveFilterBackupPeers(changes.peersToBeResolved);
|
||||
}
|
||||
|
||||
FiltersCacheController::rebuildCache();
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
struct ApplyChanges
|
||||
{
|
||||
std::vector<RegexFilter> newFilters;
|
||||
std::vector<QString> removeFiltersById;
|
||||
std::vector<std::vector<char>> removeFiltersById;
|
||||
|
||||
std::vector<RegexFilter> filtersOverrides;
|
||||
|
||||
std::vector<RegexFilterGlobalExclusion> newExclusions;
|
||||
std::vector<RegexFilterGlobalExclusion> removeExclusions;
|
||||
|
||||
std::map<long long, QString> peersToBeResolved;
|
||||
std::vector<QString> peersToBeResolved;
|
||||
|
||||
bool operator==(const ApplyChanges &) const = default;
|
||||
};
|
||||
@@ -45,7 +45,7 @@ public:
|
||||
FilterUtils &operator=(FilterUtils &&) = delete;
|
||||
|
||||
void importFromLink(const QString &link);
|
||||
bool importFromJson(const QByteArray &json);
|
||||
void importFromJson(const QByteArray &json);
|
||||
|
||||
void publishFilters();
|
||||
static QString exportFilters();
|
||||
@@ -57,14 +57,11 @@ private:
|
||||
: _manager(std::make_unique<QNetworkAccessManager>()) {
|
||||
}
|
||||
|
||||
bool handleResponse(const QByteArray &response);
|
||||
void 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;
|
||||
};
|
||||
|
||||
@@ -156,7 +156,7 @@ void sendMedia(
|
||||
Api::MessageToSend &&message,
|
||||
bool sendImagesAsPhotos) {
|
||||
if (const auto document = primaryMedia->document(); document && document->sticker()) {
|
||||
AyuSync::sendStickerSync(session, message, document);
|
||||
AyuSync::sendStickerSync(session, std::move(message), document);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -245,12 +245,17 @@ void intelligentForward(
|
||||
const Api::SendAction &action,
|
||||
const Data::ResolvedForwardDraft &draft) {
|
||||
const auto history = action.history;
|
||||
crl::on_main([&]
|
||||
const auto topicRootId = action.replyTo.topicRootId;
|
||||
const auto monoforumPeerId = action.replyTo.monoforumPeerId;
|
||||
crl::on_main([=]
|
||||
{
|
||||
history->setForwardDraft(action.replyTo.topicRootId, action.replyTo.monoforumPeerId, {});
|
||||
history->setForwardDraft(topicRootId, monoforumPeerId, {});
|
||||
});
|
||||
|
||||
const auto items = draft.items;
|
||||
if (items.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto peer = history->peer;
|
||||
|
||||
auto chunks = std::vector<ForwardChunk>();
|
||||
@@ -312,9 +317,11 @@ void forwardMessages(
|
||||
const auto history = action.history;
|
||||
const auto peer = history->peer;
|
||||
|
||||
crl::on_main([&]
|
||||
const auto topicRootId = action.replyTo.topicRootId;
|
||||
const auto monoforumPeerId = action.replyTo.monoforumPeerId;
|
||||
crl::on_main([=]
|
||||
{
|
||||
history->setForwardDraft(action.replyTo.topicRootId, action.replyTo.monoforumPeerId, {});
|
||||
history->setForwardDraft(topicRootId, monoforumPeerId, {});
|
||||
});
|
||||
|
||||
std::shared_ptr<ForwardState> state;
|
||||
@@ -378,10 +385,10 @@ void forwardMessages(
|
||||
}
|
||||
|
||||
if (!mediaDownloadable(item->media())) {
|
||||
AyuSync::sendMessageSync(session, message);
|
||||
AyuSync::sendMessageSync(session, std::move(message));
|
||||
} else if (const auto media = item->media()) {
|
||||
if (media->poll()) {
|
||||
AyuSync::sendMessageSync(session, message);
|
||||
AyuSync::sendMessageSync(session, std::move(message));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -226,8 +226,9 @@ void loadPhotoSync(not_null<Main::Session*> session, const std::pair<not_null<Ph
|
||||
}
|
||||
}
|
||||
|
||||
void sendMessageSync(not_null<Main::Session*> session, Api::MessageToSend &message) {
|
||||
crl::on_main([=, &message]
|
||||
void sendMessageSync(not_null<Main::Session*> session, Api::MessageToSend &&message) {
|
||||
const auto action = message.action;
|
||||
crl::on_main([=, message = std::move(message)]() mutable
|
||||
{
|
||||
// we cannot send events to objects
|
||||
// owned by a different thread
|
||||
@@ -237,7 +238,7 @@ void sendMessageSync(not_null<Main::Session*> session, Api::MessageToSend &messa
|
||||
});
|
||||
|
||||
|
||||
waitForMsgSync(session, message.action);
|
||||
waitForMsgSync(session, action);
|
||||
}
|
||||
|
||||
void waitForMsgSync(not_null<Main::Session*> session, const Api::SendAction &action) {
|
||||
@@ -286,10 +287,10 @@ void sendDocumentSync(not_null<Main::Session*> session,
|
||||
}
|
||||
|
||||
void sendStickerSync(not_null<Main::Session*> session,
|
||||
Api::MessageToSend &message,
|
||||
Api::MessageToSend &&message,
|
||||
not_null<DocumentData*> document) {
|
||||
auto &action = message.action;
|
||||
crl::on_main([&]
|
||||
const auto action = message.action;
|
||||
crl::on_main([=, message = std::move(message)]() mutable
|
||||
{
|
||||
Api::SendExistingDocument(std::move(message), document, std::nullopt);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ QString pathForSave(not_null<Main::Session*> session);
|
||||
QString filePath(not_null<Main::Session*> session, const Data::Media *media);
|
||||
void loadDocuments(not_null<Main::Session*> session, const std::vector<not_null<HistoryItem*>> &items);
|
||||
bool isMediaDownloadable(Data::Media *media);
|
||||
void sendMessageSync(not_null<Main::Session*> session, Api::MessageToSend &message);
|
||||
void sendMessageSync(not_null<Main::Session*> session, Api::MessageToSend &&message);
|
||||
|
||||
void sendDocumentSync(not_null<Main::Session*> session,
|
||||
Ui::PreparedGroup &group,
|
||||
@@ -32,7 +32,7 @@ void sendDocumentSync(not_null<Main::Session*> session,
|
||||
const Api::SendAction &action);
|
||||
|
||||
void sendStickerSync(not_null<Main::Session*> session,
|
||||
Api::MessageToSend &message,
|
||||
Api::MessageToSend &&message,
|
||||
not_null<DocumentData*> document);
|
||||
void waitForMsgSync(not_null<Main::Session*> session, const Api::SendAction &action);
|
||||
void loadPhotoSync(not_null<Main::Session*> session, const std::pair<not_null<PhotoData*>, FullMsgId> &photos);
|
||||
|
||||
@@ -464,12 +464,16 @@ void ShowMessageShotBox(
|
||||
not_null<Window::SessionController*> controller,
|
||||
const MessageIdsList &ids,
|
||||
Fn<void()> clearSelected) {
|
||||
const auto messages = ranges::views::all(ids)
|
||||
| ranges::views::transform([=](const auto item)
|
||||
{
|
||||
return resolveMessage(item);
|
||||
})
|
||||
| ranges::to_vector;
|
||||
auto messages = std::vector<not_null<HistoryItem*>>();
|
||||
messages.reserve(ids.size());
|
||||
for (const auto item : ids) {
|
||||
if (const auto message = resolveMessage(item)) {
|
||||
messages.push_back(message);
|
||||
}
|
||||
}
|
||||
if (messages.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const AyuFeatures::MessageShot::ShotConfig config = {
|
||||
controller,
|
||||
@@ -502,7 +506,7 @@ void WrapperImpl(
|
||||
}
|
||||
|
||||
ShowMessageShotBox(
|
||||
[=](const auto item) { return gsl::not_null(session->data().message(item)); },
|
||||
[=](const auto item) { return session->data().message(item); },
|
||||
controller,
|
||||
items,
|
||||
std::move(clearSelected));
|
||||
|
||||
@@ -44,8 +44,10 @@ void FillImportFiltersBox(not_null<Ui::GenericBox*> box, bool import) {
|
||||
|
||||
Ui::InputField *importURLField = nullptr;
|
||||
Ui::SlideWrap<Ui::VerticalLayout> *importURLWrap = nullptr;
|
||||
const auto clipboardText = QGuiApplication::clipboard()->text().trimmed();
|
||||
const auto clipboardHasUrl = import && clipboardText.startsWith("http");
|
||||
|
||||
const auto intoURL = std::make_shared<RadioenumGroup<bool>>(false);
|
||||
const auto intoURL = std::make_shared<RadioenumGroup<bool>>(clipboardHasUrl);
|
||||
const auto addOption = [&](bool value, const QString &text)
|
||||
{
|
||||
inner->add(
|
||||
@@ -58,9 +60,6 @@ void FillImportFiltersBox(not_null<Ui::GenericBox*> box, bool import) {
|
||||
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,
|
||||
@@ -73,10 +72,12 @@ void FillImportFiltersBox(not_null<Ui::GenericBox*> box, bool import) {
|
||||
container,
|
||||
st::defaultInputField,
|
||||
rpl::single(QString("URL")),
|
||||
prefill
|
||||
clipboardHasUrl ? clipboardText : QString()
|
||||
)
|
||||
);
|
||||
importURLWrap->hide(anim::type::instant);
|
||||
if (!clipboardHasUrl) {
|
||||
importURLWrap->hide(anim::type::instant);
|
||||
}
|
||||
}
|
||||
};
|
||||
addOption(false, import ? tr::ayu_FiltersImportClipboard(tr::now) : tr::ayu_FiltersExportClipboard(tr::now));
|
||||
|
||||
@@ -234,7 +234,7 @@ void AddAyuGramActions(PeerData *peerData,
|
||||
return;
|
||||
}
|
||||
|
||||
const auto topic = peerData->isForum() ? thread->asTopic() : nullptr;
|
||||
const auto topic = peerData->isForum() && thread ? thread->asTopic() : nullptr;
|
||||
const auto topicId = topic ? topic->rootId().bare : 0;
|
||||
|
||||
addCallback(Window::PeerMenuCallback::Args{
|
||||
@@ -274,11 +274,12 @@ void AddAyuGramActions(PeerData *peerData,
|
||||
tr::ayu_ViewDeletedMenuText(tr::now),
|
||||
[=]
|
||||
{
|
||||
sessionController->session().tryResolveWindow()
|
||||
->showSection(std::make_shared<MessageHistory::SectionMemento>(
|
||||
if (const auto window = sessionController->session().tryResolveWindow()) {
|
||||
window->showSection(std::make_shared<MessageHistory::SectionMemento>(
|
||||
peerData,
|
||||
nullptr,
|
||||
topicId));
|
||||
}
|
||||
},
|
||||
&st::menuIconArchive);
|
||||
if (showFilters || filteredToggleShown.value_or(false)) addAction({ .isSeparator = true });
|
||||
@@ -478,9 +479,10 @@ void AddHistoryAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
|
||||
tr::ayu_EditsHistoryMenuText(tr::now),
|
||||
[=]
|
||||
{
|
||||
item->history()->session().tryResolveWindow()
|
||||
->showSection(
|
||||
if (const auto window = item->history()->session().tryResolveWindow()) {
|
||||
window->showSection(
|
||||
std::make_shared<MessageHistory::SectionMemento>(item->history()->peer, item, 0));
|
||||
}
|
||||
},
|
||||
&st::ayuEditsHistoryIcon);
|
||||
}
|
||||
|
||||
@@ -74,9 +74,7 @@ void PerDialogFiltersListController::prepareShadowBan() {
|
||||
const auto &shadowBanned = settings.shadowBanIds();
|
||||
|
||||
for (const auto id : shadowBanned) {
|
||||
auto row = std::make_unique<PerDialogFiltersListRow>(id);
|
||||
|
||||
delegate()->peerListAppendRow(reinterpret_cast<std::unique_ptr<PeerListRow>&&>(row));
|
||||
delegate()->peerListAppendRow(std::make_unique<PerDialogFiltersListRow>(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +116,7 @@ void PerDialogFiltersListController::prepare() {
|
||||
|
||||
row->setCustomStatus(status, false);
|
||||
|
||||
delegate()->peerListAppendRow(reinterpret_cast<std::unique_ptr<PeerListRow>&&>(row));
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
}
|
||||
|
||||
// sortByName();
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
#include "inline_bots/bot_attach_web_view.h"
|
||||
#include "settings/settings_builder.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "styles/style_ayu_icons.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "ui/vertical_list.h"
|
||||
@@ -233,9 +235,9 @@ void AyuFilters::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
|
||||
&st::menuIconUnarchive);
|
||||
}
|
||||
addAction({ .isSeparator = true });
|
||||
addAction(
|
||||
tr::ayu_FiltersMenuClear(tr::now),
|
||||
[=] {
|
||||
addAction({
|
||||
.text = tr::ayu_FiltersMenuClear(tr::now),
|
||||
.handler = [=] {
|
||||
auto callback = [=](Fn<void()> &&close) {
|
||||
AyuDatabase::deleteAllFilters();
|
||||
AyuDatabase::deleteAllExclusions();
|
||||
@@ -246,11 +248,14 @@ void AyuFilters::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
|
||||
auto box = Ui::MakeConfirmBox({
|
||||
.text = tr::ayu_FiltersClearPopupText(),
|
||||
.confirmed = callback,
|
||||
.confirmText = tr::ayu_FiltersClearPopupActionText()
|
||||
.confirmText = tr::ayu_FiltersClearPopupActionText(),
|
||||
.confirmStyle = &st::attentionBoxButton,
|
||||
});
|
||||
Ui::show(std::move(box));
|
||||
},
|
||||
&st::menuIconClear);
|
||||
.icon = &st::menuIconClearAttention,
|
||||
.isAttention = true,
|
||||
});
|
||||
}
|
||||
|
||||
AyuFilters::AyuFilters(
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
#include "ui/toast/toast.h"
|
||||
#include "window/window_controller.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <latch>
|
||||
#include <QTimer>
|
||||
@@ -1223,49 +1224,6 @@ void applyLocalPremiumEmoji(TextWithEntities &text) {
|
||||
}
|
||||
}
|
||||
|
||||
void resolveAllChats(const std::map<long long, QString> &peers) {
|
||||
auto session = currentSession();
|
||||
|
||||
crl::async([=, &session]
|
||||
{
|
||||
while (!peers.empty()) {
|
||||
for (const auto &[id, username] : peers) {
|
||||
auto latch = std::make_shared<TimedCountDownLatch>(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->countDown();
|
||||
};
|
||||
auto onFail = [=, &latch](const MTP::Error &error)
|
||||
{
|
||||
if (MTP::IsFloodError(error.type())) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(20));
|
||||
}
|
||||
latch->countDown();
|
||||
};
|
||||
|
||||
session->api().checkChatInvite(username, onSuccess, onFail);
|
||||
latch->await(std::chrono::seconds(20));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
not_null<Main::Session*> currentSession() {
|
||||
return &Core::App().domain().active().session();
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@ bool mediaDownloadable(const Data::Media* media);
|
||||
TextWithEntities reverseLocalPremiumEmoji(const TextWithEntities &text, not_null<History *> history, bool isForQuote = false);
|
||||
void applyLocalPremiumEmoji(TextWithEntities &text);
|
||||
|
||||
void resolveAllChats(const std::map<long long, QString> &peers);
|
||||
not_null<Main::Session *> currentSession();
|
||||
|
||||
PeerData* getPeerFromDialogId(ID id);
|
||||
|
||||
@@ -31,13 +31,11 @@ void processIcon(QString shortcut, QString iconPath) {
|
||||
if (SUCCEEDED(hr)) {
|
||||
hr = pShellLink->QueryInterface(IID_IPersistFile, (void**) &pPersistFile);
|
||||
if (SUCCEEDED(hr)) {
|
||||
WCHAR wszShortcutPath[MAX_PATH];
|
||||
shortcut.toWCharArray(wszShortcutPath);
|
||||
wszShortcutPath[shortcut.length()] = '\0';
|
||||
const auto shortcutPath = shortcut.toStdWString();
|
||||
|
||||
if (SUCCEEDED(pPersistFile->Load(wszShortcutPath, STGM_READWRITE))) {
|
||||
if (SUCCEEDED(pPersistFile->Load(shortcutPath.c_str(), STGM_READWRITE))) {
|
||||
pShellLink->SetIconLocation(iconPath.toStdWString().c_str(), 0);
|
||||
pPersistFile->Save(wszShortcutPath, TRUE);
|
||||
pPersistFile->Save(shortcutPath.c_str(), TRUE);
|
||||
}
|
||||
|
||||
pPersistFile->Release();
|
||||
|
||||
@@ -92,6 +92,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtCore/QMimeData>
|
||||
|
||||
// AyuGram includes
|
||||
#include "ayu/features/filters/filters_cache_controller.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
|
||||
|
||||
@@ -500,6 +501,23 @@ ListWidget::ListWidget(
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
rpl::merge(
|
||||
_session->changes().peerUpdates(
|
||||
Data::PeerUpdate::Flag::IsBlocked
|
||||
) | rpl::to_empty,
|
||||
FiltersCacheController::updates()
|
||||
) | rpl::on_next([=] {
|
||||
crl::on_main(this, [=] {
|
||||
if (_viewsCapacity.empty()) {
|
||||
for (const auto &view : _items) {
|
||||
view->setPendingResize();
|
||||
}
|
||||
const auto old = _slice;
|
||||
refreshRows(old);
|
||||
}
|
||||
});
|
||||
}, lifetime());
|
||||
|
||||
_session->downloaderTaskFinished(
|
||||
) | rpl::on_next([=] {
|
||||
update();
|
||||
|
||||
@@ -791,11 +791,10 @@ void MainMenu::setupMenu() {
|
||||
MarkAsReadChatList(chats);
|
||||
|
||||
// slight delay for forums to send packets
|
||||
dispatchToMainThread([=]() mutable
|
||||
{
|
||||
dispatchToMainThread(crl::guard(controller, [=] {
|
||||
auto &ghost = AyuSettings::ghost(&controller->session());
|
||||
ghost.setSendReadMessages(prev);
|
||||
}, 200);
|
||||
}), 200);
|
||||
close();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user