diff --git a/src/edit.cpp b/src/edit.cpp new file mode 100644 index 0000000..64829a5 --- /dev/null +++ b/src/edit.cpp @@ -0,0 +1,953 @@ +#include "edit.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace edit +{ + // ========================================================= + // Internal helpers + // ========================================================= + + namespace + { + using Clock = std::chrono::steady_clock; + + const auto startTime = Clock::now(); + + std::mt19937& rng() + { + static std::mt19937 generator{ + std::random_device{}() + }; + + return generator; + } + + std::string formatDuration( + std::chrono::seconds seconds + ) + { + long long total = seconds.count(); + + const long long days = total / 86400; + total %= 86400; + + const long long hours = total / 3600; + total %= 3600; + + const long long minutes = total / 60; + const long long secs = total % 60; + + std::ostringstream output; + + if (days > 0) + output << days << "d "; + + if (hours > 0 || days > 0) + output << hours << "h "; + + if (minutes > 0 || hours > 0 || days > 0) + output << minutes << "m "; + + output << secs << "s"; + + return output.str(); + } + + std::string currentTime() + { + const std::time_t now = std::time(nullptr); + + std::tm tm{}; + +#ifdef _WIN32 + localtime_s(&tm, &now); +#else + localtime_r(&now, &tm); +#endif + + std::ostringstream output; + + output << std::put_time( + &tm, + "%Y-%m-%d %H:%M:%S" + ); + + return output.str(); + } + + dpp::embed baseEmbed( + const std::string& title, + const std::string& description + ) + { + dpp::embed embed; + + embed + .set_title(title) + .set_description(description) + .set_timestamp(time(nullptr)); + + return embed; + } + + void replyEmbed( + const dpp::slashcommand_t& event, + const dpp::embed& embed + ) + { + event.reply( + dpp::message() + .add_embed(embed) + ); + } + + bool isAdministrator( + const dpp::slashcommand_t& event + ) + { + return event.command.member.has_permission( + event.command.guild_id, + dpp::p_administrator + ); + } + + bool isModerator( + const dpp::slashcommand_t& event + ) + { + return + isAdministrator(event) || + event.command.member.has_permission( + event.command.guild_id, + dpp::p_manage_messages + ); + } + + std::string userName( + const dpp::slashcommand_t& event + ) + { + return event.command.get_issuing_user().username; + } + } + + // ========================================================= + // CommandManager + // ========================================================= + + void CommandManager::registerCommand( + const std::string& command, + CommandCallback callback + ) + { + commands[command] = std::move(callback); + } + + bool CommandManager::handleCommand( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + const std::string commandName = + event.command.get_command_name(); + + const auto it = commands.find(commandName); + + if (it == commands.end()) + return false; + + it->second(bot, event); + + return true; + } + + // ========================================================= + // /ping + // ========================================================= + + void ping( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + event.reply( + dpp::message("🏓 Pong!") + .set_flags(dpp::m_ephemeral) + ); + } + + // ========================================================= + // /hello + // ========================================================= + + void hello( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + const std::string name = userName(event); + + event.reply( + "Hello, **" + name + "**! 👋" + ); + } + + // ========================================================= + // /status + // ========================================================= + + void status( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + const auto uptime = + std::chrono::duration_cast( + Clock::now() - startTime + ); + + dpp::embed embed = + baseEmbed( + "Bot Status", + "Current status of the bot." + ); + + embed.add_field( + "Status", + "🟢 Online", + true + ); + + embed.add_field( + "Uptime", + formatDuration(uptime), + true + ); + + embed.add_field( + "Time", + currentTime(), + true + ); + + replyEmbed(event, embed); + } + + // ========================================================= + // /uptime + // ========================================================= + + void uptime( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + const auto elapsed = + std::chrono::duration_cast( + Clock::now() - startTime + ); + + event.reply( + "⏱️ Uptime: **" + + formatDuration(elapsed) + + "**" + ); + } + + // ========================================================= + // /about + // ========================================================= + + void about( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + dpp::embed embed = + baseEmbed( + "About the Bot", + "A C++ Discord bot powered by DPP." + ); + + embed.add_field( + "Language", + "C++", + true + ); + + embed.add_field( + "Library", + "DPP", + true + ); + + embed.add_field( + "Purpose", + "Community utilities and moderation.", + false + ); + + replyEmbed(event, embed); + } + + // ========================================================= + // /cpp + // ========================================================= + + void cpp( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + dpp::embed embed = + baseEmbed( + "C++", + "C++ is a compiled general-purpose programming language." + ); + + embed.add_field( + "Core topics", + "Classes, templates, pointers, references, STL, RAII.", + false + ); + + embed.add_field( + "Useful STL", + "`vector`, `string`, `unordered_map`, `optional`.", + false + ); + + replyEmbed(event, embed); + } + + // ========================================================= + // /help + // ========================================================= + + void help( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + dpp::embed embed = + baseEmbed( + "Help", + "Available utility commands." + ); + + embed.add_field( + "General", + "`/ping`\n" + "`/hello`\n" + "`/status`\n" + "`/uptime`\n" + "`/about`\n" + "`/cpp`", + false + ); + + embed.add_field( + "Fun", + "`/coinflip`\n" + "`/roll`\n" + "`/8ball`\n" + "`/choose`", + false + ); + + embed.add_field( + "Server", + "`/serverinfo`\n" + "`/userinfo`\n" + "`/avatar`", + false + ); + + replyEmbed(event, embed); + } + + // ========================================================= + // /coinflip + // ========================================================= + + void coinflip( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + std::uniform_int_distribution distribution(0, 1); + + const bool heads = distribution(rng()) == 0; + + event.reply( + heads + ? "🪙 **Heads!**" + : "🪙 **Tails!**" + ); + } + + // ========================================================= + // /roll + // ========================================================= + + void roll( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + int sides = 6; + + const auto parameter = + event.get_parameter("sides"); + + if (std::holds_alternative(parameter)) + { + sides = + static_cast( + std::get(parameter) + ); + } + + sides = std::clamp(sides, 2, 1000); + + std::uniform_int_distribution distribution( + 1, + sides + ); + + const int result = distribution(rng()); + + event.reply( + "🎲 You rolled **" + + std::to_string(result) + + "** / " + + std::to_string(sides) + ); + } + + // ========================================================= + // /8ball + // ========================================================= + + void eightBall( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + static const std::vector answers = { + "Yes.", + "No.", + "Definitely.", + "Probably.", + "Maybe.", + "Ask again later.", + "The outlook is good.", + "The outlook is unclear." + }; + + std::uniform_int_distribution distribution( + 0, + answers.size() - 1 + ); + + event.reply( + "🎱 " + answers[distribution(rng())] + ); + } + + // ========================================================= + // /choose + // ========================================================= + + void choose( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + const auto first = + std::get( + event.get_parameter("first") + ); + + const auto second = + std::get( + event.get_parameter("second") + ); + + std::uniform_int_distribution distribution( + 0, + 1 + ); + + const std::string result = + distribution(rng()) == 0 + ? first + : second; + + event.reply( + "🤔 I choose **" + + result + + "**." + ); + } + + // ========================================================= + // /serverinfo + // ========================================================= + + void serverInfo( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + const dpp::guild* guild = + dpp::find_guild(event.command.guild_id); + + if (!guild) + { + event.reply( + dpp::message( + "Unable to find this server." + ).set_flags(dpp::m_ephemeral) + ); + + return; + } + + dpp::embed embed = + baseEmbed( + "Server Information", + guild->name + ); + + embed.add_field( + "Members", + std::to_string(guild->member_count), + true + ); + + embed.add_field( + "Channels", + std::to_string(guild->channels.size()), + true + ); + + embed.add_field( + "Roles", + std::to_string(guild->roles.size()), + true + ); + + embed.add_field( + "Server ID", + std::to_string(guild->id), + false + ); + + replyEmbed(event, embed); + } + + // ========================================================= + // /userinfo + // ========================================================= + + void userInfo( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + dpp::snowflake userId = + event.command.get_issuing_user().id; + + const auto parameter = + event.get_parameter("user"); + + if (std::holds_alternative(parameter)) + { + userId = + std::get(parameter); + } + + const dpp::user* user = + dpp::find_user(userId); + + if (!user) + { + event.reply( + dpp::message( + "Unable to find that user." + ).set_flags(dpp::m_ephemeral) + ); + + return; + } + + dpp::embed embed = + baseEmbed( + "User Information", + user->username + ); + + embed.add_field( + "Username", + user->username, + true + ); + + embed.add_field( + "ID", + std::to_string(user->id), + true + ); + + embed.add_field( + "Bot", + user->is_bot() ? "Yes" : "No", + true + ); + + embed.set_thumbnail( + user->get_avatar_url( + 256 + ) + ); + + replyEmbed(event, embed); + } + + // ========================================================= + // /avatar + // ========================================================= + + void avatar( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + const dpp::user& user = + event.command.get_issuing_user(); + + dpp::embed embed = + baseEmbed( + "Avatar", + user.username + ); + + embed.set_image( + user.get_avatar_url(1024) + ); + + replyEmbed(event, embed); + } + + // ========================================================= + // /echo + // ========================================================= + + void echo( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + const auto text = + std::get( + event.get_parameter("text") + ); + + if (text.empty()) + { + event.reply( + dpp::message( + "You need to provide some text." + ).set_flags(dpp::m_ephemeral) + ); + + return; + } + + event.reply(text); + } + + // ========================================================= + // /announce + // ========================================================= + + void announce( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + if (!isModerator(event)) + { + event.reply( + dpp::message( + "You don't have permission to use this command." + ).set_flags(dpp::m_ephemeral) + ); + + return; + } + + const auto text = + std::get( + event.get_parameter("text") + ); + + dpp::message message( + text + ); + + bot.message_create( + dpp::snowflake(event.command.channel_id), + message + ); + + event.reply( + dpp::message( + "Announcement sent." + ).set_flags(dpp::m_ephemeral) + ); + } + + // ========================================================= + // /clear + // ========================================================= + + void clear( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + if (!isModerator(event)) + { + event.reply( + dpp::message( + "You don't have permission to use this command." + ).set_flags(dpp::m_ephemeral) + ); + + return; + } + + const auto amount = + std::get( + event.get_parameter("amount") + ); + + const int count = + static_cast( + std::clamp( + amount, + 1, + 100 + ) + ); + + event.reply( + dpp::message( + "Clear command received for " + + std::to_string(count) + + " messages." + ).set_flags(dpp::m_ephemeral) + ); + } + + // ========================================================= + // /lock + // ========================================================= + + void lock( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + if (!isModerator(event)) + { + event.reply( + dpp::message( + "You don't have permission to use this command." + ).set_flags(dpp::m_ephemeral) + ); + + return; + } + + event.reply( + dpp::message( + "🔒 Channel lock requested." + ).set_flags(dpp::m_ephemeral) + ); + } + + // ========================================================= + // /unlock + // ========================================================= + + void unlock( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + if (!isModerator(event)) + { + event.reply( + dpp::message( + "You don't have permission to use this command." + ).set_flags(dpp::m_ephemeral) + ); + + return; + } + + event.reply( + dpp::message( + "🔓 Channel unlock requested." + ).set_flags(dpp::m_ephemeral) + ); + } + + // ========================================================= + // /modcheck + // ========================================================= + + void modCheck( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ) + { + if (isModerator(event)) + { + event.reply( + dpp::message( + "✅ You have moderator permissions." + ).set_flags(dpp::m_ephemeral) + ); + } + else + { + event.reply( + dpp::message( + "❌ You don't have moderator permissions." + ).set_flags(dpp::m_ephemeral) + ); + } + } + + // ========================================================= + // Register all edit.cpp commands + // ========================================================= + + void registerCommands(CommandManager& manager) + { + manager.registerCommand( + "ping", + ping + ); + + manager.registerCommand( + "hello", + hello + ); + + manager.registerCommand( + "status", + status + ); + + manager.registerCommand( + "uptime", + uptime + ); + + manager.registerCommand( + "about", + about + ); + + manager.registerCommand( + "cpp", + cpp + ); + + manager.registerCommand( + "help", + help + ); + + manager.registerCommand( + "coinflip", + coinflip + ); + + manager.registerCommand( + "roll", + roll + ); + + manager.registerCommand( + "8ball", + eightBall + ); + + manager.registerCommand( + "choose", + choose + ); + + manager.registerCommand( + "serverinfo", + serverInfo + ); + + manager.registerCommand( + "userinfo", + userInfo + ); + + manager.registerCommand( + "avatar", + avatar + ); + + manager.registerCommand( + "echo", + echo + ); + + manager.registerCommand( + "announce", + announce + ); + + manager.registerCommand( + "clear", + clear + ); + + manager.registerCommand( + "lock", + lock + ); + + manager.registerCommand( + "unlock", + unlock + ); + + manager.registerCommand( + "modcheck", + modCheck + ); + } +} diff --git a/src/edit.h b/src/edit.h new file mode 100644 index 0000000..4510d4e --- /dev/null +++ b/src/edit.h @@ -0,0 +1,40 @@ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace edit +{ + using CommandCallback = + std::function; + + class CommandManager + { + private: + std::unordered_map< + std::string, + CommandCallback + > commands; + + public: + void registerCommand( + const std::string& command, + CommandCallback callback + ); + + bool handleCommand( + dpp::cluster& bot, + const dpp::slashcommand_t& event + ); + }; + + void registerCommands(CommandManager& manager); +} diff --git a/src/main.cpp b/src/main.cpp index c1bacad..16ea930 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,12 +1,16 @@ #include -#include #include +#include +#include + #include +#include #include "commands/commands.h" #include "globals/globals.h" #include "utils/suggestion/suggestion.h" #include "utils/moderation/moderation.h" +#include "edit.h" using json = nlohmann::json; @@ -15,88 +19,295 @@ std::vector cmdList = { { "beginner", "Get a beginner's guide to C++", cmd::beginnerCommand }, { "coding", "Get a coding question", cmd::codingCommand }, { "close", "Close a ticket or forum post", cmd::closeCommand }, - { "ticket", "Open a ticket", cmd::ticketCommand, { dpp::command_option(dpp::command_option_type::co_user, "participant", "Add participant", false) }}, + + { + "ticket", + "Open a ticket", + cmd::ticketCommand, + { + dpp::command_option( + dpp::command_option_type::co_user, + "participant", + "Add participant", + false + ) + } + }, + { "code", "Formatting code on Discord", cmd::codeCommand }, { "project", "Get a project idea", cmd::projectCommand }, - { "rule", "Get the server rules", cmd::ruleCommand, { dpp::command_option(dpp::command_option_type::co_integer, "number", "Rule to mention", false) }} + + { + "rule", + "Get the server rules", + cmd::ruleCommand, + { + dpp::command_option( + dpp::command_option_type::co_integer, + "number", + "Rule to mention", + false + ) + } + } }; int main() { + std::cout << "[*] Starting bot..." << std::endl; + + // Load config std::ifstream configFile("config.json"); - json config = json::parse(configFile); + if (!configFile.is_open()) + { + std::cerr << "[!] Could not open config.json" << std::endl; + return 1; + } + + json config; + + try + { + configFile >> config; + } + catch (const json::parse_error& e) + { + std::cerr + << "[!] Invalid config.json: " + << e.what() + << std::endl; + + return 1; + } + + // Check token + if (!config.contains("token") || + !config["token"].is_string() || + config["token"].get().empty()) + { + std::cerr << "[!] Discord token is missing." << std::endl; + return 1; + } + + // Load globals std::string globalsConfigError; + if (!globals::loadFromConfig(config, globalsConfigError)) { - std::cerr << "[!] Invalid configuration: " << globalsConfigError << std::endl; + std::cerr + << "[!] Invalid configuration: " + << globalsConfigError + << std::endl; + return 1; } - dpp::cluster bot(config["token"], dpp::i_default_intents | dpp::i_message_content); + // Create bot + dpp::cluster bot( + config["token"].get(), + 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; - bot.set_presence(dpp::presence(dpp::presence_status::ps_online, dpp::activity_type::at_watching, "cppdiscord.com")); + // Edit command system + edit::CommandManager editCommands; + edit::registerCommands(editCommands); + + // --------------------------------------------------------- + // READY + // --------------------------------------------------------- + + bot.on_ready([&bot](const dpp::ready_t& event) + { + std::cout + << "[+] Bot ready as " + << bot.me.username + << std::endl; + + bot.set_presence( + dpp::presence( + dpp::presence_status::ps_online, + dpp::activity_type::at_watching, + "cppdiscord.com" + ) + ); if (dpp::run_once()) { + std::cout + << "[*] Registering slash commands..." + << std::endl; + std::vector slashcommands; + for (const auto& item : cmdList) { - dpp::slashcommand slashCommand; - slashCommand.set_name(item.name); - slashCommand.set_description(item.desc); - slashCommand.set_application_id(bot.me.id); + dpp::slashcommand command; + + command + .set_name(item.name) + .set_description(item.desc) + .set_application_id(bot.me.id); - for (const dpp::command_option& arg : item.args) - slashCommand.add_option(arg); + for (const auto& option : item.args) + command.add_option(option); if (item.permissions) - slashCommand.set_default_permissions(dpp::permission(item.permissions)); + { + command.set_default_permissions( + dpp::permission(item.permissions) + ); + } - slashcommands.push_back(slashCommand); + slashcommands.push_back(command); } - bot.global_bulk_command_create(slashcommands); + + bot.global_bulk_command_create( + slashcommands, + [](const dpp::confirmation_callback_t& callback) + { + if (callback.is_error()) + { + std::cerr + << "[!] Failed to register commands: " + << callback.get_error().message + << std::endl; + } + else + { + std::cout + << "[+] Slash commands registered." + << std::endl; + } + } + ); } }); - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - for (const auto& item : cmdList) + // --------------------------------------------------------- + // SLASH COMMANDS + // --------------------------------------------------------- + + bot.on_slashcommand( + [&bot, &editCommands](const dpp::slashcommand_t& event) { - if (item.name == event.command.get_command_name()) + const std::string commandName = + event.command.get_command_name(); + + std::cout + << "[COMMAND] /" + << commandName + << std::endl; + + // Handle commands from edit.cpp + if (editCommands.handleCommand(bot, event)) + return; + + // Handle original commands + for (const auto& item : cmdList) { - item.function(bot, event); + if (item.name == commandName) + { + item.function(bot, event); + return; + } + } + + std::cerr + << "[!] Unknown command: /" + << commandName + << std::endl; + } + ); + + // --------------------------------------------------------- + // MESSAGE CREATE + // --------------------------------------------------------- + + 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) + return; + + if (channel->name == "suggestions") + { + utils::suggestion::createSuggestion( + bot, + event + ); } } - }); + ); - bot.on_message_create([&bot, &moderationService](const dpp::message_create_t& event) { - if (moderationService.handleMessage(event)) - return; + // --------------------------------------------------------- + // BUTTONS + // --------------------------------------------------------- - const dpp::channel* channel = dpp::find_channel(event.msg.channel_id); + bot.on_button_click( + [&bot](const dpp::button_click_t& event) + { + if (event.custom_id == "delSuggestion") + { + utils::suggestion::deleteSuggestion( + bot, + event + ); + } + else if (event.custom_id == "editSuggestion") + { + utils::suggestion::editSuggestion( + bot, + event + ); + } + else if ( + event.custom_id.starts_with("hint_button_") + ) + { + cmd::handleProjectHintButton( + bot, + event + ); + } + } + ); - if (channel && channel->name == "suggestions") - utils::suggestion::createSuggestion(bot, event); - }); + // --------------------------------------------------------- + // MODALS + // --------------------------------------------------------- - bot.on_button_click([&bot](const dpp::button_click_t& event) { - if (event.custom_id == "delSuggestion") - utils::suggestion::deleteSuggestion(bot, event); - else if (event.custom_id == "editSuggestion") - utils::suggestion::editSuggestion(bot, event); - else if (event.custom_id.starts_with("hint_button_")) - cmd::handleProjectHintButton(bot, event); - }); + bot.on_form_submit( + [&bot](const dpp::form_submit_t& event) + { + if (event.custom_id == "editModal") + { + utils::suggestion::showSuggestionEditModal( + bot, + event + ); + } + } + ); - bot.on_form_submit([&bot](const dpp::form_submit_t& event) { - if (event.custom_id == "editModal") - utils::suggestion::showSuggestionEditModal(bot, event); - }); + // --------------------------------------------------------- + // START + // --------------------------------------------------------- + + std::cout + << "[*] Connecting to Discord..." + << std::endl; bot.start(dpp::st_wait); + return 0; }