Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
cmake_minimum_required(VERSION 3.3)
cmake_minimum_required(VERSION 3.25)

project(bot LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(dpp CONFIG REQUIRED)

include(FetchContent)

FetchContent_Declare(
dpp
GIT_REPOSITORY https://github.com/brainboxdotcc/DPP.git
GIT_TAG v10.1.4
)

FetchContent_MakeAvailable(dpp)

file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/src/config.json DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/src/res DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
Expand All @@ -29,8 +38,10 @@ add_executable(bot
# utils
src/utils/suggestion/suggestion.cpp
src/utils/suggestion/suggestion.h
src/utils/moderation/moderation.cpp
src/utils/moderation/moderation.h
)

target_link_libraries(bot
PRIVATE dpp::dpp
PRIVATE dpp
)
3 changes: 2 additions & 1 deletion src/commands/rule_cmd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <dpp/appcommand.h>
#include <dpp/dpp.h>
#include <dpp/channel.h>
#include <cstdint>
#include <string>
#include <vector>

Expand Down Expand Up @@ -31,7 +32,7 @@ void cmd::ruleCommand(dpp::cluster& bot, const dpp::slashcommand_t& event)
if (option.type != dpp::co_integer)
return; // should never happen

const long index = std::get<long>(option.value);
const auto index = std::get<std::int64_t>(option.value);

if ((index < 1 || index > rules.size()))
return event.reply(dpp::message("Rule number " + std::to_string(index) + " does not exist. Visit <#" + globals::channel::rulesId.str() + "> to see all available rules.").set_flags(dpp::m_ephemeral));
Expand Down
2 changes: 2 additions & 0 deletions src/globals/globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace globals
namespace channel
{
static constexpr dpp::snowflake rulesId = 1130464978860785705;
static constexpr dpp::snowflake jailId = 1513269975844917409;
}

namespace category
Expand All @@ -29,6 +30,7 @@ namespace globals
namespace role
{
static constexpr dpp::snowflake staffId = 1130473404345110621;
static constexpr dpp::snowflake jailId = 1506351798900887582;
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "commands/commands.h"
#include "utils/suggestion/suggestion.h"
#include "utils/moderation/moderation.h"

using json = nlohmann::json;

Expand All @@ -24,6 +25,7 @@ int main()
json config = json::parse(configFile);

dpp::cluster bot(config["token"], dpp::i_default_intents | dpp::i_message_content);
ModerationService moderationService(bot);

bot.on_ready([&bot](const dpp::ready_t& event) {
std::cout << "[!] Bot ready" << std::endl;
Expand Down Expand Up @@ -62,7 +64,10 @@ int main()
}
});

bot.on_message_create([&bot](const dpp::message_create_t& event) {
bot.on_message_create([&bot, &moderationService](const dpp::message_create_t& event) {
if (moderationService.handleMessage(event))
return;

const dpp::channel* channel = dpp::find_channel(event.msg.channel_id);

if (channel && channel->name == "suggestions")
Expand Down
126 changes: 126 additions & 0 deletions src/utils/moderation/moderation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#include "moderation.h"
#include "../../globals/globals.h"

ModerationService::ModerationService(dpp::cluster& bot) : bot(bot) {}

void ModerationService::cleanupOldEntries(const std::chrono::steady_clock::time_point& now)
{
const auto threshold = now - cleanupThreshold;

std::erase_if(state, [&](const auto& entry)
{
return entry.second.lastPostTime < threshold;
});
}

std::string ModerationService::makeMessageSignature(const dpp::message& msg)
{
std::string signature = msg.content;

for (const auto& att : msg.attachments)
{
signature += '|';
signature += att.filename;
signature += '|';
signature += std::to_string(att.size);
}

for (const auto& emb : msg.embeds)
{
signature += '|';
signature += emb.title;
signature += '|';
signature += emb.description;
signature += '|';
signature += emb.url;
}

return signature;
}

bool ModerationService::handleMessage(const dpp::message_create_t& event)
{
if (event.msg.author.is_bot())
return false;

if (event.msg.content.empty() &&
event.msg.attachments.empty() &&
event.msg.embeds.empty())
return false;

const auto now = std::chrono::steady_clock::now();
const auto userId = event.msg.author.id;
const std::string signature = makeMessageSignature(event.msg);

bool warn = false;
bool jail = false;
std::vector<PostedMessage> toDelete;
dpp::snowflake targetChannel{};

{
std::scoped_lock lock(mtx);
cleanupOldEntries(now);

auto& userState = state[userId];
const bool sameMessage = (signature == userState.lastSignature);
const bool withinWindow = (now - userState.lastPostTime) <= crossPostWindow;
userState.lastPostTime = now;

if (!sameMessage || !withinWindow)
{
userState.lastSignature = signature;
userState.messages.clear();
}

userState.messages.push_back({event.msg.channel_id, event.msg.id});

const std::size_t count = userState.messages.size();

if (count == 2)
{
warn = true;
targetChannel = event.msg.channel_id;
}
else if (count >= 3)
{
jail = true;
toDelete = std::move(userState.messages);
state.erase(userId);
}
}

if (warn)
{
bot.message_delete(event.msg.id, targetChannel);

dpp::message warnMessage(targetChannel,
dpp::utility::user_mention(userId) + " " + std::string(warningMsg));
warnMessage.set_allowed_mentions(false, false, false, false, {userId}, {});
bot.message_create(warnMessage);

return true;
}

if (jail)
{
for (const auto& m : toDelete)
{
bot.message_delete(m.messageId, m.channelId);
}

bot.guild_member_add_role(
event.msg.guild_id,
userId,
globals::role::jailId
);

dpp::message jailMessage(globals::channel::jailId,
dpp::utility::user_mention(userId) + " " + std::string(jailMsg));
jailMessage.set_allowed_mentions(false, false, false, false, {userId}, {});
bot.message_create(jailMessage);

return true;
}

return false;
}
59 changes: 59 additions & 0 deletions src/utils/moderation/moderation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#ifndef MODERATION_H
#define MODERATION_H

#include <dpp/dpp.h>
#include <chrono>
#include <string>
#include <unordered_map>
#include <vector>
#include <mutex>

class ModerationService
{
public:
explicit ModerationService(dpp::cluster& bot);

/**
* @brief Analyzes the message for cross-posting/spam
* Duplicate content by same user in:
* - 2 channels: Delete the message and send a warning
* - 3+ channels: Delete all messages and jail the user
* @param event message create event
* @return true if the message was deleted/handled, false otherwise
*/
bool handleMessage(const dpp::message_create_t& event);

private:
static constexpr auto crossPostWindow = std::chrono::seconds{10};
static constexpr auto cleanupThreshold = std::chrono::seconds{180};

static constexpr std::string_view warningMsg =
"You posted the same message on multiple channels. "
"Your message has been deleted, because we do not allow cross-posting.";

static constexpr std::string_view jailMsg =
"You have been jailed as you posted the same message on multiple channels. "
"Please contact a staff member if you think this is a mistake.";

struct PostedMessage
{
dpp::snowflake channelId{};
dpp::snowflake messageId{};
};

struct UserState
{
std::string lastSignature;
std::chrono::steady_clock::time_point lastPostTime{};
std::vector<PostedMessage> messages;
};

static std::string makeMessageSignature(const dpp::message& msg);
void cleanupOldEntries(const std::chrono::steady_clock::time_point& now);

dpp::cluster& bot;
std::mutex mtx;
std::unordered_map<dpp::snowflake, UserState> state;
};

#endif // MODERATION_H
Loading