From 2e46a8a736a90b3e391813d02e2aece19d7cd3f8 Mon Sep 17 00:00:00 2001 From: royna2544 Date: Tue, 19 Nov 2024 01:19:43 +0900 Subject: [PATCH] Refactor Spamblocker Eject unneeded srcs from tests Fix some unused param warnings --- CMakeLists.txt | 44 ++- src/api/CommandModule.cpp | 6 +- src/api/TgBotApiImpl.cpp | 11 +- src/global_handlers/SpamBlockManager.cpp | 68 ++++ src/global_handlers/SpamBlocker.cpp | 292 ++++-------------- src/include/LogSinks.hpp | 2 +- src/include/ManagedThreads.hpp | 2 +- src/include/api/Providers.hpp | 2 +- src/include/global_handlers/RegEXHandler.hpp | 2 - src/include/global_handlers/SpamBlock.hpp | 161 ++++++---- .../global_handlers/SpamBlockManager.hpp | 23 ++ src/include/trivial_helpers/_tgbot.h | 4 +- src/main.cpp | 2 +- src/ml/ChatDataCollector.cpp | 2 +- src/socket/include/TgBotSocket_Export.hpp | 6 +- .../interface/impl/bot/SocketDataHandler.cpp | 2 +- src/third-party/tgbot-cpp | 2 +- src/utils/libfs.hpp | 2 +- tests/CMakeLists.txt | 13 +- tests/SocketDataHandlerTest.cpp | 15 +- tests/commands/CommandModulesTest.cpp | 2 - tests/commands/DatabaseCmdTest.cpp | 1 - tests/mocks/Random.hpp | 2 +- 23 files changed, 326 insertions(+), 340 deletions(-) create mode 100644 src/global_handlers/SpamBlockManager.cpp create mode 100644 src/include/global_handlers/SpamBlockManager.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ed265919..951b1843 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -284,6 +284,7 @@ find_package(ZLIB REQUIRED) find_package(Protobuf) find_package(fmt REQUIRED) find_package(jsoncpp REQUIRED) # Required by tgbot-cpp anyways... +find_package(CURL REQUIRED) # If we couldn't find protobuf or dirty abseil fix is enabled. if (DIRTY_ABSEIL_FIX OR NOT Protobuf_FOUND) # Then we can just include source-abseil @@ -355,9 +356,35 @@ add_my_library( ) ################### The Bot's main functionaility ################### +add_my_library( + NAME Regex + SRCS + src/global_handlers/RegEXHandler.cpp + LIBS absl::status + STATIC +) + add_my_library( NAME PPImpl - SRCS + SRCS + src/Authorization.cpp + src/api/CommandModule.cpp + src/global_handlers/SpamBlocker.cpp + src/global_handlers/ChatObserver.cpp + src/socket/interface/impl/bot/SocketDataHandler.cpp + src/socket/interface/impl/bot/TgBotSocketInterface.cpp + LIBS TgBot TgBotUtils TgBotDBImpl + TgBotPPImpl_shared_deps TgBotStringRes ${CMAKE_DL_LIBS} TgBotSocket JsonCpp::JsonCpp + CURL::libcurl + STATIC +) +##################################################################### + +################# The Bot's main launcher (program) ################# +add_my_executable( + NAME main + SRCS + src/main.cpp src/ManagedThread.cpp src/ThreadManager.cpp src/Authorization.cpp @@ -375,6 +402,7 @@ add_my_library( src/api/components/UnknownCommand.cpp src/global_handlers/RegEXHandler.cpp src/global_handlers/SpamBlocker.cpp + src/global_handlers/SpamBlockManager.cpp src/global_handlers/ChatObserver.cpp src/ml/ChatDataCollector.cpp src/web/TgBotWebServer.cpp @@ -383,17 +411,9 @@ add_my_library( src/socket/interface/impl/bot/TgBotSocketInterface.cpp src/socket/interface/impl/backends/ServerBackend.cpp src/socket/interface/impl/backends/ServerBackend_${TARGET_VARIANT}.cpp - LIBS TgBot TgBotUtils TgBotWeb TgBotDBImpl absl::status TgBotRandom TgBot_restartfmt_parser - TgBotPPImpl_shared_deps TgBotStringRes ${CMAKE_DL_LIBS} TgBotSocket JsonCpp::JsonCpp TgBotsighandler - STATIC -) -##################################################################### - -################# The Bot's main launcher (program) ################# -add_my_executable( - NAME main - SRCS src/main.cpp - LIBS TgBotPPImpl TgBotDBLoading TgBot_restartfmt_parser fruit + LIBS TgBotDBLoading TgBot_restartfmt_parser fruit TgBot TgBotUtils TgBotWeb TgBotDBImpl + absl::status TgBotRandom TgBot_restartfmt_parser CURL::libcurl TgBotRegex + TgBotPPImpl_shared_deps TgBotStringRes ${CMAKE_DL_LIBS} TgBotSocket JsonCpp::JsonCpp TgBotsighandler ) if (UNIX) diff --git a/src/api/CommandModule.cpp b/src/api/CommandModule.cpp index c75055a7..945c8666 100644 --- a/src/api/CommandModule.cpp +++ b/src/api/CommandModule.cpp @@ -35,7 +35,7 @@ class DLWrapper { RAIIHandle underlying() { RAIIHandle tmp(nullptr, &dlclose); std::swap(handle, tmp); - return std::move(tmp); + return tmp; } // dlfcn functions. @@ -57,7 +57,7 @@ class DLWrapper { }; CommandModule::CommandModule(std::filesystem::path filePath) - : filePath(std::move(filePath)), handle(nullptr, &dlclose) {} + : handle(nullptr, &dlclose), filePath(std::move(filePath)) {} bool CommandModule::load() { if (handle != nullptr) { @@ -103,7 +103,7 @@ bool CommandModule::load() { _module->isEnforced(), _module->name, fmt::ptr(modulePtr)); #endif - handle = std::move(dlwrapper.underlying()); + handle = dlwrapper.underlying(); return true; } diff --git a/src/api/TgBotApiImpl.cpp b/src/api/TgBotApiImpl.cpp index 89dbbaa0..ea24e1cc 100644 --- a/src/api/TgBotApiImpl.cpp +++ b/src/api/TgBotApiImpl.cpp @@ -4,10 +4,15 @@ #include #include +#include +#include +#include +#include #include #include #include #include +#include #include #include #include @@ -26,12 +31,6 @@ #include #include -#include "ConfigManager.hpp" -#include "StringResLoader.hpp" -#include "api/CommandModule.hpp" -#include "api/MessageExt.hpp" -#include "api/components/FileCheck.hpp" - bool TgBotApiImpl::validateValidArgs(const DynModule* module, MessageExt::Ptr& message) { if (!module->valid_args.enabled) { diff --git a/src/global_handlers/SpamBlockManager.cpp b/src/global_handlers/SpamBlockManager.cpp new file mode 100644 index 00000000..e7a5c432 --- /dev/null +++ b/src/global_handlers/SpamBlockManager.cpp @@ -0,0 +1,68 @@ +#include +#include +#include + +#include + +void SpamBlockManager::runFunction(const std::stop_token &token) { + while (!token.stop_requested()) { + consumeAndDetect(); + delayUnlessStop(sSpamDetectDelay); + } +} + +void SpamBlockManager::onDetected(ChatId chat, UserId user, + std::vector messageIds) const { + // Initial set - all false set + static auto perms = std::make_shared(); + switch (_config) { + case Config::DELETE_AND_MUTE: + LOG(INFO) << fmt::format("Try mute offending user"); + try { + _api->muteChatMember(chat, user, perms, + to_secs(kMuteDuration).count()); + } catch (const TgBot::TgException &e) { + LOG(WARNING) << fmt::format("Cannot mute: {}", e.what()); + } + [[fallthrough]]; + case Config::DELETE: { + try { + _api->deleteMessages(chat, messageIds); + } catch (const TgBot::TgException &e) { + DLOG(INFO) << "Error deleting messages: " << e.what(); + } + [[fallthrough]]; + } + case Config::LOGGING_ONLY: + SpamBlockBase::onDetected(chat, user, messageIds); + break; + default: + break; + }; +} + +bool SpamBlockManager::shouldBeSkipped(const Message::Ptr &message) const { + if (_auth->isAuthorized(message, AuthContext::Flags::None)) { + return true; + } + + // Ignore old messages + if (!AuthContext::isUnderTimeLimit(message)) { + return true; + } + + // Bot's PM is not a concern + if (message->chat->type == TgBot::Chat::Type::Private) { + return true; + } + return false; +} + +SpamBlockManager::SpamBlockManager(TgBotApi::Ptr api, AuthContext *auth) + : _api(api), _auth(auth) { + api->onAnyMessage([this](TgBotApi::CPtr, const Message::Ptr &message) { + addMessage(message); + return TgBotApi::AnyMessageResult::Handled; + }); + run(); +} \ No newline at end of file diff --git a/src/global_handlers/SpamBlocker.cpp b/src/global_handlers/SpamBlocker.cpp index b041d120..2938916e 100644 --- a/src/global_handlers/SpamBlocker.cpp +++ b/src/global_handlers/SpamBlocker.cpp @@ -6,264 +6,82 @@ #include #include #include -#include +#include #include -#include +#include #include "Types.h" -using std::chrono_literals::operator""s; -using TgBot::ChatPermissions; - -template -typename Container::iterator _findItImpl( - Container &c, - std::function fn, - typename Type::Ptr t) { - return std::ranges::find_if( - c, [=](const auto &it) { return fn(it)->id == t->id; }); -} - -template -typename Container::iterator findChatIt( - Container &c, - std::function fn, - Chat::Ptr t) { - return _findItImpl(c, fn, t); -} - -template -typename Container::iterator findUserIt( - Container &c, - std::function fn, - User::Ptr t) { - return _findItImpl(c, fn, t); -} - -std::string SpamBlockBase::commonMsgdataFn(const Message::Ptr &m) { - if (m->sticker) { - return m->sticker->fileUniqueId; - } else if (m->animation) { - return m->animation->fileUniqueId; - } else { - return m->text.value_or(""); - } -} - -bool SpamBlockBase::isEntryOverThreshold(PerChatHandle::const_reference t, - const size_t threshold) { - const size_t kEntryValue = t.second.size(); - const bool isOverThreshold = kEntryValue >= threshold; - DLOG_IF(INFO, isOverThreshold) - << "Note: Value " << kEntryValue << " is over threshold " << threshold; - return isOverThreshold; -} - -void SpamBlockBase::_logSpamDetectCommon(PerChatHandle::const_reference t, - const char *name) { - LOG(INFO) << fmt::format("Spam detected for user {}, filtered by {}", - t.first, name); -} - -void SpamBlockBase::takeAction(OneChatIterator it, const PerChatHandle &map, - const size_t threshold, const char *name) { - for (const auto &mapmsg : map) { - handleUserAndMessagePair(mapmsg, it, threshold, name); - } +void SpamBlockBase::onDetected(ChatId chat, UserId user, + std::vector /*messageIds*/) const { + LOG(INFO) << fmt::format("Spam detected for chat {}, by user {}", + chat_map.at(chat), user_map.at(user)); } -void SpamBlockBase::spamDetectFunc(OneChatIterator handle) { - PerChatHandle MaxSameMsgMap; - PerChatHandle MaxMsgMap; - // By most msgs sent by that user - for (const auto &perUser : handle->second) { - std::apply( - [&](const auto &first, const auto &second) { - MaxMsgMap.emplace(first, second); - }, - perUser); - } - - for (const auto &pair : handle->second) { - std::multimap byMsgContent; - for (const auto &obj : pair.second) { - byMsgContent.emplace(commonMsgdataFn(obj), obj); - } - std::unordered_map> commonMap; - for (const auto &cnt : byMsgContent) { - commonMap[cnt.first].emplace_back(cnt.second); - } - // Find the most common value - auto mostCommonIt = std::ranges::max_element( - commonMap, [](const auto &lhs, const auto &rhs) { - return lhs.second.size() < rhs.second.size(); - }); - MaxSameMsgMap.emplace(pair.first, mostCommonIt->second); - } - - takeAction(handle, MaxSameMsgMap, sMaxSameMsgThreshold, "MaxSameMsg"); - takeAction(handle, MaxMsgMap, sMaxMsgThreshold, "MaxMsg"); -} - -void SpamBlockBase::runFunction(const std::stop_token &token) { - while (!token.stop_requested()) { - { - const std::lock_guard _(buffer_m); - if (buffer_sub.size() > 0) { - auto its = buffer_sub.begin(); - while (its != buffer_sub.end()) { - const auto it = findChatIt( - buffer, [](const auto &it) { return it.first; }, - its->first); - if (it == buffer.end()) { - its = buffer_sub.erase(its); - continue; - } - if (its->second >= sSpamDetectThreshold) { - LOG(INFO) << fmt::format( - "Launching spamdetect for chat {}", its->first); - spamDetectFunc(it); - } - buffer.erase(it); - its->second = 0; - ++its; +void SpamBlockBase::setConfig(Config config) { _config = config; } + +void SpamBlockBase::consumeAndDetect() { + const std::lock_guard _(mutex); + for (const auto &[chat, per_chat_map] : chat_messages_data) { + int count = + std::accumulate(per_chat_map.begin(), per_chat_map.end(), 0, + [](const int /*index*/, const auto &messages) { + return messages.second.size(); + }); + if (count >= sSpamDetectThreshold) { + const auto &chatPtr = chat_map.at(chat); + LOG(INFO) << fmt::format( + "Launching spam detection in {}: Detected {}.", chatPtr, count); + // Run detection + for (auto it = per_chat_map.cbegin(); it != per_chat_map.cend(); + it++) { + std::vector msgids; + std::ranges::transform(it->second, std::back_inserter(msgids), + [](const auto &x) { return x.first; }); + if (Matcher::detect(it) || + Matcher::detect(it)) { + onDetected(chat, it->first, msgids); } } } - delayUnlessStop(10s); } + chat_messages_data.clear(); } -void SpamBlockBase::addMessage(const Message::Ptr &message) { - static std::once_flag once; +void SpamBlockBase::addMessage(Message::Ptr message) { + // Always ignore when spamblock is off + if (_config == Config::CTRL_OFF) { + return; + } // Run possible additional checks if (shouldBeSkipped(message)) { return; } - std::call_once(once, [this] { run(); }); - - { - const std::lock_guard _(buffer_m); - auto bufferIt = findChatIt( - buffer, [](const auto &it) { return it.first; }, message->chat); - if (bufferIt != buffer.end()) { - const auto bufferUserIt = findUserIt( - bufferIt->second, [](const auto &it) { return it.first; }, - message->from); - - if (bufferUserIt != bufferIt->second.end()) { - bufferUserIt->second.emplace_back(message); - } else { - bufferIt->second[message->from].emplace_back(message); - } - } else { - buffer[message->chat][message->from].emplace_back(message); - } - const auto bufferSubIt = findChatIt( - buffer_sub, [](const auto &it) { return it.first; }, message->chat); - if (bufferSubIt != buffer_sub.end()) { - ++bufferSubIt->second; - } else { - ++buffer_sub[message->chat]; - } - } -} - -void SpamBlockManager::handleUserAndMessagePair(PerChatHandleConstRef e, - OneChatIterator it, - const size_t threshold, - const char *name) { - bool enforce = false; - switch (spamBlockConfig) { - case CtrlSpamBlock::CTRL_ENFORCE: - enforce = true; - [[fallthrough]]; - case CtrlSpamBlock::CTRL_ON: { - _deleteAndMuteCommon(it, e, threshold, name, enforce); - break; - } - case CtrlSpamBlock::CTRL_LOGGING_ONLY_ON: - if (isEntryOverThreshold(e, threshold)) { - _logSpamDetectCommon(e, name); - } - break; - default: - break; - }; -} - -void SpamBlockManager::_deleteAndMuteCommon(const OneChatIterator &handle, - PerChatHandle::const_reference t, - const size_t threshold, - const char *name, const bool mute) { - // Initial set - all false set - static auto perms = std::make_shared(); - if (isEntryOverThreshold(t, threshold)) { - _logSpamDetectCommon(t, name); - - if (!t.first->username) { - _api->sendMessage( - handle->first->id, - fmt::format("Spam detected @{}", t.first->username.value())); - } - std::vector message_ids; - std::ranges::for_each(t.second, [&message_ids](auto &&messageIn) { - message_ids.emplace_back(messageIn->messageId); - }); - try { - _api->deleteMessages(handle->first->id, message_ids); - } catch (const TgBot::TgException &e) { - DLOG(INFO) << "Error deleting message: " << e.what(); - } - if (mute) { - LOG(INFO) << fmt::format("Try mute user {} in chat {}", t.first, - handle->first); - try { - _api->muteChatMember(handle->first->id, t.first->id, perms, - to_secs(kMuteDuration).count()); - } catch (const TgBot::TgException &e) { - LOG(WARNING) - << fmt::format("Cannot mute user {} in chat {}: {}", - t.first, handle->first, e.what()); - } - } - } -} - -bool SpamBlockManager::shouldBeSkipped(const Message::Ptr &message) const { - if (_auth->isAuthorized(message, AuthContext::Flags::None)) { - return true; - } - - // Global cfg - if (spamBlockConfig == CtrlSpamBlock::CTRL_OFF) { - return true; - } - - // Ignore old messages - if (!AuthContext::isUnderTimeLimit(message)) { - return true; - } - - // We care GIF, sticker, text spams only, or if it isn't fowarded msg + // We cares GIF, sticker, text spams only, or if it isn't fowarded msg + // A required check. if ((!message->animation && !message->text && !message->sticker) || message->forwardOrigin) { - return true; + return; } - // Bot's PM is not a concern - if (message->chat->type == TgBot::Chat::Type::Private) { - return true; + std::string messageData; + if (message->text) { + messageData = *message->text; + } else if (message->animation){ + messageData = message->animation->fileUniqueId; + } else if (message->sticker) { + messageData = message->sticker->fileUniqueId; } - return false; -} -SpamBlockManager::SpamBlockManager(TgBotApi::Ptr api, AuthContext *auth) - : _api(api), _auth(auth) { - api->onAnyMessage([this](TgBotApi::CPtr, const Message::Ptr &message) { - addMessage(message); - return TgBotApi::AnyMessageResult::Handled; - }); -} \ No newline at end of file + ChatId chatId = message->chat->id; + UserId userId = message->from->id; + MessageId messageId = message->messageId; + { + const std::lock_guard _(mutex); + chat_messages_data[chatId][userId].emplace_back(messageId, messageData); + chat_map[chatId] = message->chat; + user_map[userId] = message->from; + } +} diff --git a/src/include/LogSinks.hpp b/src/include/LogSinks.hpp index 89cea437..df655f12 100644 --- a/src/include/LogSinks.hpp +++ b/src/include/LogSinks.hpp @@ -54,7 +54,7 @@ struct FileSinkBase : absl::LogSink { void Send(const absl::LogEntry& entry) override { const std::lock_guard lock(m); if (entry.log_severity() < absl::LogSeverity::kError) { - file.puts(entry.text_message_with_prefix_and_newline().data()); + (void)file.puts(entry.text_message_with_prefix_and_newline().data()); } } FileSinkBase() = default; diff --git a/src/include/ManagedThreads.hpp b/src/include/ManagedThreads.hpp index 77875f8d..360e3a5e 100644 --- a/src/include/ManagedThreads.hpp +++ b/src/include/ManagedThreads.hpp @@ -147,7 +147,7 @@ T* ThreadManager::create(Usage usage, Args&&... args) { std::lock_guard lock(mControllerLock); - DLOG(INFO) << fmt::format("MGR: Starting {}...", usage); + LOG(INFO) << fmt::format("MGR: Starting {}...", usage); if constexpr (sizeof...(args) != 0) { newIt = std::make_unique(std::forward(args)...); } else { diff --git a/src/include/api/Providers.hpp b/src/include/api/Providers.hpp index 202d7034..8a7a3be1 100644 --- a/src/include/api/Providers.hpp +++ b/src/include/api/Providers.hpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include diff --git a/src/include/global_handlers/RegEXHandler.hpp b/src/include/global_handlers/RegEXHandler.hpp index 4fef941f..c6dc7fff 100644 --- a/src/include/global_handlers/RegEXHandler.hpp +++ b/src/include/global_handlers/RegEXHandler.hpp @@ -6,8 +6,6 @@ #include #include #include -#include -#include #include /** diff --git a/src/include/global_handlers/SpamBlock.hpp b/src/include/global_handlers/SpamBlock.hpp index 5c777edb..b261dfd0 100644 --- a/src/include/global_handlers/SpamBlock.hpp +++ b/src/include/global_handlers/SpamBlock.hpp @@ -1,78 +1,129 @@ #pragma once +#include +#include + #include -#include +#include #include +#include +#include #include #include #include -#include -#include "trivial_helpers/fruit_inject.hpp" - -using namespace TgBotSocket::data; using TgBot::Chat; using TgBot::Message; using TgBot::User; -struct SpamBlockBase : ManagedThreadRunnable { - // User and array of message pointers sent by that user - using PerChatHandle = std::map>; - // Iterator type of buffer object, which contains >> map - using OneChatIterator = std::map::const_iterator; - using PerChatHandleConstRef = PerChatHandle::const_reference; - using ManagedThreadRunnable::ManagedThreadRunnable; - constexpr static int sMaxSameMsgThreshold = 3; - constexpr static int sMaxMsgThreshold = 5; +struct SpamBlockBase { + // This is a per-chat map, containing UserId and the vector of pair of + // messageid-messagecontent + using UserMessagesMap = + std::map>>; + using Config = TgBotSocket::data::CtrlSpamBlock; + + // Triggered when a chat has more than sSpamDetectThreshold messages + // In sSpamDetectDelay delay. constexpr static int sSpamDetectThreshold = 5; + constexpr static std::chrono::seconds sSpamDetectDelay{10}; - ~SpamBlockBase() override = default; - virtual void handleUserAndMessagePair(PerChatHandleConstRef e, - OneChatIterator it, - const size_t threshold, - const char *name) {}; - virtual bool shouldBeSkipped(const Message::Ptr &msg) const = 0; + // Describing a SpamBlockDetector. + class Matcher { + public: + virtual ~Matcher() = default; - void runFunction(const std::stop_token& token) override; - void addMessage(const Message::Ptr &message); + // Describes threshold for spam detection. + // Need to be redeclared in the child scope. + static constexpr int kThreshold = 0; - static std::string commonMsgdataFn(const Message::Ptr &m); + // Declares the name of this Matcher. + // Need to be redeclared in the child scope. + static constexpr std::string_view name; - CtrlSpamBlock spamBlockConfig = CtrlSpamBlock::CTRL_ON; + // Returns the count of messages per user, that matches the criteria. + static int count(const UserMessagesMap::const_iterator entry) { + return 0; + } - protected: - static bool isEntryOverThreshold(PerChatHandleConstRef t, - const size_t threshold); - static void _logSpamDetectCommon(PerChatHandleConstRef t, const char *name); + template T> + static bool detect(const UserMessagesMap::const_iterator entry) { + static_assert(!T::name.empty(), "Must have a name"); + static_assert(T::kThreshold != 0, "Threshold must be positive"); + int count = T::count(entry); + if (count >= T::kThreshold) { + LOG(INFO) << fmt::format( + "Detected: {} Value {} is over threshold {}", T::name, + count, T::kThreshold); + } + return count >= T::kThreshold; + } + }; - private: - void spamDetectFunc(OneChatIterator handle); - void takeAction(OneChatIterator handle, const PerChatHandle &map, - const size_t threshold, const char *name); - std::map buffer; - std::map buffer_sub; - std::mutex buffer_m; // Protect buffer, buffer_sub -}; + class SameMessageMatcher : public Matcher { + public: + static constexpr int kThreshold = 3; + static constexpr std::string_view name = "SameMessageMatcher"; + static int count(const UserMessagesMap::const_iterator entry) { + std::map kSameMessageMap; + for (const auto &elem : entry->second) { + const auto &[id, content] = elem; + if (!kSameMessageMap.contains(content)) { + kSameMessageMap[content] = 1; + } else { + ++kSameMessageMap[content]; + } + } + return std::ranges::max_element( + kSameMessageMap, + [](const auto &smsg, const auto &rmsg) { + return smsg.second > rmsg.second; + }) + ->second; + } + }; + + class MessageCountMatcher : public Matcher { + public: + static constexpr int kThreshold = 5; + static constexpr std::string_view name = "MessageCountMatcher"; + static int count(const UserMessagesMap::const_iterator entry) { + return static_cast(entry->second.size()); + } + }; + + SpamBlockBase() = default; + ~SpamBlockBase() = default; -struct SpamBlockManager : SpamBlockBase { - APPLE_INJECT(SpamBlockManager(TgBotApi::Ptr api, AuthContext *auth)); - ~SpamBlockManager() override = default; + // Pure virtual function, hooks before the message is added. + // Returns true if the message should be skipped, false otherwise. + // Dummy version: Returns false. + virtual bool shouldBeSkipped(const Message::Ptr & /*msg*/) const { + return false; + } - using SpamBlockBase::run; - using SpamBlockBase::runFunction; - void handleUserAndMessagePair(PerChatHandleConstRef e, OneChatIterator it, - const size_t threshold, - const char *name) override; - // Additional hook for handling messages - // that should be handled differently - // (e.g., delete messages, mute users) - bool shouldBeSkipped(const Message::Ptr &message) const override; + // Set the SpamBlock config. Based on SpamBlockBase::Config. + virtual void setConfig(Config config); + + // Function called when the SpamBlock framework detects spamming user. + // Arguments passed: ChatId, UserId, Offending messageIds + virtual void onDetected(ChatId chat, UserId user, + std::vector messageIds) const; + + // Add a message to the buffer. + void addMessage(Message::Ptr message); + + // Run the SpamBlock framework. + void consumeAndDetect(); private: - constexpr static auto kMuteDuration = std::chrono::minutes(3); - void _deleteAndMuteCommon(const OneChatIterator &handle, - PerChatHandleConstRef t, const size_t threshold, - const char *name, const bool mute); - TgBotApi::Ptr _api; - AuthContext *_auth; -}; \ No newline at end of file + std::map chat_messages_data; + + // Cache these for easy lookup + std::map chat_map; + std::map user_map; + + mutable std::mutex mutex; // Protect above maps + protected: + Config _config = Config::DELETE; +}; diff --git a/src/include/global_handlers/SpamBlockManager.hpp b/src/include/global_handlers/SpamBlockManager.hpp new file mode 100644 index 00000000..ce1e6fce --- /dev/null +++ b/src/include/global_handlers/SpamBlockManager.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "SpamBlock.hpp" + +struct SpamBlockManager : SpamBlockBase, ManagedThreadRunnable { + APPLE_INJECT(SpamBlockManager(TgBotApi::Ptr api, AuthContext *auth)); + ~SpamBlockManager() override = default; + + void runFunction(const std::stop_token &token) override; + void onDetected(ChatId chat, UserId user, + std::vector messageIds) const override; + // Additional hook for handling messages + // that should be handled differently + // (e.g., delete messages, mute users) + bool shouldBeSkipped(const Message::Ptr &message) const override; + + private: + constexpr static auto kMuteDuration = std::chrono::minutes(3); + TgBotApi::Ptr _api; + AuthContext *_auth; +}; \ No newline at end of file diff --git a/src/include/trivial_helpers/_tgbot.h b/src/include/trivial_helpers/_tgbot.h index eff7c83c..2f0c9a86 100644 --- a/src/include/trivial_helpers/_tgbot.h +++ b/src/include/trivial_helpers/_tgbot.h @@ -43,10 +43,10 @@ struct fmt::formatter : formatter { chat->username.value_or("unknown")); case Chat::Type::Channel: return fmt::format_to(ctx.out(), "Channel (@{})", - chat->username.value_or("unknown")); + chat->title.value_or("unknown")); case Chat::Type::Supergroup: return fmt::format_to(ctx.out(), "Group (@{})", - chat->username.value_or("unknown")); + chat->title.value_or("unknown")); default: return fmt::format_to(ctx.out(), "Unknown chat"); } diff --git a/src/main.cpp b/src/main.cpp index ecbf3408..91591d39 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,7 +30,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/src/ml/ChatDataCollector.cpp b/src/ml/ChatDataCollector.cpp index 386615e1..54bc9250 100644 --- a/src/ml/ChatDataCollector.cpp +++ b/src/ml/ChatDataCollector.cpp @@ -6,7 +6,7 @@ #include ChatDataCollector::Data::Data(const Message::Ptr& message) { - if (!message->text) { + if (message->text) { msgType = Data::MsgType::TEXT; } else if (!message->photo.empty()) { msgType = Data::MsgType::PHOTO; diff --git a/src/socket/include/TgBotSocket_Export.hpp b/src/socket/include/TgBotSocket_Export.hpp index a1fc4c39..6b2c171c 100644 --- a/src/socket/include/TgBotSocket_Export.hpp +++ b/src/socket/include/TgBotSocket_Export.hpp @@ -165,9 +165,9 @@ enum class FileType { enum class CtrlSpamBlock { CTRL_OFF, // Disabled - CTRL_LOGGING_ONLY_ON, // Logging only, not taking action - CTRL_ON, // Enabled, does delete but doesn't mute - CTRL_ENFORCE, // Enabled, deletes and mutes + LOGGING_ONLY, // Logging only, not taking action + DELETE, // Enabled, does delete but doesn't mute + DELETE_AND_MUTE, // Enabled, deletes and mutes CTRL_MAX, }; diff --git a/src/socket/interface/impl/bot/SocketDataHandler.cpp b/src/socket/interface/impl/bot/SocketDataHandler.cpp index 0b43c880..1c1c44c9 100644 --- a/src/socket/interface/impl/bot/SocketDataHandler.cpp +++ b/src/socket/interface/impl/bot/SocketDataHandler.cpp @@ -77,7 +77,7 @@ GenericAck SocketInterfaceTgBot::handle_WriteMsgToChatId(const void* ptr) { GenericAck SocketInterfaceTgBot::handle_CtrlSpamBlock(const void* ptr) { const auto* data = static_cast(ptr); - spamblock->spamBlockConfig = *data; + spamblock->setConfig(*data); return GenericAck::ok(); } diff --git a/src/third-party/tgbot-cpp b/src/third-party/tgbot-cpp index 4aea259b..95bf1950 160000 --- a/src/third-party/tgbot-cpp +++ b/src/third-party/tgbot-cpp @@ -1 +1 @@ -Subproject commit 4aea259b32ecbd1ff28168a75c433168753aa0f4 +Subproject commit 95bf195049aca3317f72925e93571745d72bedf7 diff --git a/src/utils/libfs.hpp b/src/utils/libfs.hpp index 7048789e..02408661 100644 --- a/src/utils/libfs.hpp +++ b/src/utils/libfs.hpp @@ -121,7 +121,7 @@ std::vector walk_up_tree_and_gather( return result; } -inline std::filesystem::path operator/(std::filesystem::path path, FS::SharedLibType test) { +inline std::filesystem::path operator/(std::filesystem::path path, FS::SharedLibType /*test*/) { if (!path.has_extension()) { path += FS::kDylibExtension; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6b92a263..7fb01f98 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,7 +6,6 @@ add_my_executable( SRCS TestMain.cpp AuthorizationTest.cpp - RegexHandlerTest.cpp ResourceManagerTest.cpp TryParseTest.cpp SharedMallocTest.cpp @@ -34,6 +33,18 @@ add_my_executable( TEST ) +add_my_executable( + NAME regex + SRCS + TestMain.cpp + RegexHandlerTest.cpp + LIBS + GTest::gtest + GTest::gmock + TgBotRegex + TEST +) + add_my_executable( NAME socketdatahandler SRCS diff --git a/tests/SocketDataHandlerTest.cpp b/tests/SocketDataHandlerTest.cpp index ac8f41b7..535f3603 100644 --- a/tests/SocketDataHandlerTest.cpp +++ b/tests/SocketDataHandlerTest.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -12,19 +13,19 @@ #include #include #include -#include -#include "mocks/TgBotApi.hpp" -#include "mocks/SocketInterfaceImpl.hpp" -#include "mocks/VFSOperations.hpp" +#include "global_handlers/SpamBlock.hpp" #include "mocks/DatabaseBase.hpp" #include "mocks/ResourceProvider.hpp" +#include "mocks/SocketInterfaceImpl.hpp" +#include "mocks/TgBotApi.hpp" +#include "mocks/VFSOperations.hpp" using testing::_; using testing::DoAll; +using testing::IsNull; using testing::Return; using testing::SaveArg; -using testing::IsNull; fruit::Component @@ -34,8 +35,8 @@ getSocketComponent() { .bind() .bind() .bind() - .bind() - .bind(); + .bind() + .registerConstructor(); } class SocketDataHandlerTest : public ::testing::Test { diff --git a/tests/commands/CommandModulesTest.cpp b/tests/commands/CommandModulesTest.cpp index 57d6918b..f4db33c1 100644 --- a/tests/commands/CommandModulesTest.cpp +++ b/tests/commands/CommandModulesTest.cpp @@ -2,12 +2,10 @@ #include -#include #include #include #include "CommandLine.hpp" -#include "Random.hpp" #include "api/CommandModule.hpp" #include "api/Providers.hpp" #include "api/TgBotApi.hpp" diff --git a/tests/commands/DatabaseCmdTest.cpp b/tests/commands/DatabaseCmdTest.cpp index fc9dc0f3..3ee23a55 100644 --- a/tests/commands/DatabaseCmdTest.cpp +++ b/tests/commands/DatabaseCmdTest.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include "CommandModulesTest.hpp" diff --git a/tests/mocks/Random.hpp b/tests/mocks/Random.hpp index c87fafee..b581384a 100644 --- a/tests/mocks/Random.hpp +++ b/tests/mocks/Random.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include class MockRandom : public RandomBase {