diff options
Diffstat (limited to 'bot/src')
| -rw-r--r-- | bot/src/handle_actions.cpp | 136 | ||||
| -rw-r--r-- | bot/src/http_webhook_server.cpp | 175 | ||||
| -rw-r--r-- | bot/src/main.cpp | 25 | ||||
| -rw-r--r-- | bot/src/utils.cpp | 215 |
4 files changed, 545 insertions, 6 deletions
diff --git a/bot/src/handle_actions.cpp b/bot/src/handle_actions.cpp new file mode 100644 index 0000000..38d9982 --- /dev/null +++ b/bot/src/handle_actions.cpp @@ -0,0 +1,136 @@ +#include <dpp/dpp.h> + +dpp::task<bool> handle_actions(const dpp::slashcommand_t &event, const nlohmann::json &actions, const std::unordered_map<std::string, std::string> &key_values) +{ + + dpp::cluster *cluster = event.owner; + dpp::user user_ptr = event.command.get_issuing_user(); + dpp::async thinking = event.co_thinking(false); + if (actions.is_array()) + { + int i = 0; + for (const auto &action : actions) + { + i++; + if (action.contains("type")) + { + std::string action_type = action["type"]; + if (action_type == "delete_messages" && event.command.is_guild_interaction()) + { + dpp::guild guild_ptr = event.command.get_guild(); + // let's retrieve the member. + dpp::guild_member member_ptr = guild_ptr.members.find(user_ptr.id)->second; + dpp::guild_member bot_member_ptr = guild_ptr.members.find(cluster->me.id)->second; + std::unordered_map<std::string, std::string> error_messages = { + {"error", "You need to wait a bit before deleting messages."}, + {"error_amount", "The amount of messages to delete must be between 1 and 100."}, + {"error_perm_channel", "You do not have permission to delete messages in this channel."}}; + + if (action.contains("error_amount")) + { + error_messages["error_amount"] = action["error_amount"].get<std::string>(); + } + + if (action.contains("error_perm_channel")) + { + error_messages["error_perm_channel"] = action["error_perm_channel"].get<std::string>(); + } + + if (action.contains("error")) + { + error_messages["error"] = action["error"].get<std::string>(); + } + // let's retrieve the current channel + const dpp::channel *channel_ptr = &event.command.get_channel(); + + // let's check if the user has permission to delete messages + if (!channel_ptr->get_user_permissions(member_ptr).has(dpp::p_manage_messages) && + !channel_ptr->get_user_permissions(bot_member_ptr).has(dpp::p_manage_messages)) + { + co_await thinking; + event.edit_response(error_messages["error_perm_channel"]); + co_return false; + } + int amount = 0; + if (action.contains("depend_on")) + { + std::string depend_on = action["depend_on"]; + auto it = key_values.find(depend_on); + if (it != key_values.end()) + { + std::string depend_on_value = it->second; + + // let's convert the depend_on_value to an int + amount = std::stoi(depend_on_value); + if (amount < 0 || amount > 100) + { + co_await thinking; + event.edit_response(error_messages["error_amount"]); + co_return false; + } + } + } + if (amount > 0) + { + dpp::confirmation_callback_t callback = co_await cluster->co_messages_get(channel_ptr->id, 0, 0, 0, amount); + if (callback.is_error()) + { + printf("Error: %s\n", callback.get_error().message.c_str()); + co_await thinking; + event.edit_response(error_messages["error"]); + co_return false; + } + auto messages = callback.get<dpp::message_map>(); + if (messages.empty()) + { + printf("No messages to delete\n"); + co_await thinking; + event.edit_response("No messages to delete."); + co_return false; + } + std::vector<dpp::snowflake> msg_ids; + + for (const auto &msg : messages) + { + // let's check if the message is older than 2 weeks + if (msg.second.get_creation_time() < dpp::utility::time_f() - 1209600) + { + printf("Message is older than 2 weeks\n"); + continue; + } + else + { + msg_ids.push_back(msg.second.id); + } + } + + if (!msg_ids.empty()) + { + dpp::confirmation_callback_t result; + if(msg_ids.size() == 1){ + result = co_await cluster->co_message_delete(msg_ids[0], channel_ptr->id); + }else{ + result = co_await cluster->co_message_delete_bulk(msg_ids, channel_ptr->id); + } + if (result.is_error()) + { + printf("Error: %s\n", result.get_error().message.c_str()); + co_await thinking; + event.edit_response(error_messages["error"]); + co_return false; + } + + co_await thinking; + } + } + } + } + if (i == actions.size()) + { + + co_await thinking; + co_return true; + } + } + } +} diff --git a/bot/src/http_webhook_server.cpp b/bot/src/http_webhook_server.cpp new file mode 100644 index 0000000..a6daa63 --- /dev/null +++ b/bot/src/http_webhook_server.cpp @@ -0,0 +1,175 @@ +#include "../include/http_webhook_server.hpp" +#include <fcntl.h> +#include <unistd.h> +#include <cstring> +#include <algorithm> + +HttpWebhookServer::HttpWebhookServer(uint16_t port, Handler handler) + : port(port), request_handler(handler) { + setupSocket(); + setupEpoll(); +} + +HttpWebhookServer::~HttpWebhookServer() { + stop(); + if (server_fd != -1) ::close(server_fd); + if (epoll_fd != -1) ::close(epoll_fd); +} + +void HttpWebhookServer::setupSocket() { + server_fd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); + if (server_fd == -1) throw std::system_error(errno, std::generic_category()); + + int opt = 1; + setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(port); + + if (::bind(server_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == -1) + throw std::system_error(errno, std::generic_category()); + + if (::listen(server_fd, 128) == -1) + throw std::system_error(errno, std::generic_category()); +} + +void HttpWebhookServer::setupEpoll() { + epoll_fd = ::epoll_create1(0); + if (epoll_fd == -1) + throw std::system_error(errno, std::generic_category()); + + epoll_event event{}; + event.events = EPOLLIN | EPOLLET; + event.data.fd = server_fd; + + if (::epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) + throw std::system_error(errno, std::generic_category()); +} + +void HttpWebhookServer::start() { + running = true; + epoll_event events[64]; + + while (running) { + int nfds = ::epoll_wait(epoll_fd, events, 64, -1); + if (nfds == -1 && errno != EINTR) + throw std::system_error(errno, std::generic_category()); + + for (int i = 0; i < nfds; ++i) { + if (events[i].data.fd == server_fd) { + while (true) { + sockaddr_in client_addr{}; + socklen_t client_len = sizeof(client_addr); + int client_fd = ::accept4(server_fd, (sockaddr*)&client_addr, &client_len, SOCK_NONBLOCK); + if (client_fd == -1) break; + + epoll_event event{}; + event.events = EPOLLIN | EPOLLET | EPOLLRDHUP; + event.data.fd = client_fd; + ::epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event); + clients[client_fd] = ClientContext{}; + } + } else { + handleClient(events[i].data.fd); + } + } + } +} + +void HttpWebhookServer::stop() { + running = false; +} + +void HttpWebhookServer::closeClient(int fd) { + ::epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, nullptr); + ::close(fd); + clients.erase(fd); +} + +void HttpWebhookServer::handleClient(int fd) { + char buffer[4096]; + ssize_t count = ::recv(fd, buffer, sizeof(buffer), MSG_DONTWAIT); + + if (count > 0) { + auto& ctx = clients[fd]; + ctx.input_buffer.append(buffer, count); + + if (ctx.input_buffer.find("\r\n\r\n") != std::string::npos) { + HttpRequest req; + parseHttpRequest(ctx, req); + + HttpResponse res = request_handler(req); + buildHttpResponse(res, ctx.output_buffer); + + epoll_event event{}; + event.events = EPOLLOUT | EPOLLET | EPOLLRDHUP; + event.data.fd = fd; + ::epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &event); + } + } + else if (count == 0 || (count == -1 && errno != EAGAIN)) { + closeClient(fd); + return; + } + + if (!clients.count(fd)) return; // Client déjà fermé plus haut + + auto& ctx = clients[fd]; + if (!ctx.output_buffer.empty()) { + ssize_t sent = ::send(fd, + ctx.output_buffer.data() + ctx.bytes_written, + ctx.output_buffer.size() - ctx.bytes_written, + MSG_DONTWAIT); + + if (sent > 0) ctx.bytes_written += sent; + if (ctx.bytes_written == ctx.output_buffer.size()) { + closeClient(fd); + } + } +} + +void HttpWebhookServer::parseHttpRequest(ClientContext& ctx, HttpRequest& req) { + std::istringstream stream(ctx.input_buffer); + std::string line; + + std::getline(stream, line); + std::istringstream req_line(line); + req_line >> req.method >> req.path; + + while (std::getline(stream, line) && line != "\r") { + size_t colon = line.find(':'); + if (colon != std::string::npos) { + std::string key = line.substr(0, colon); + std::string value = line.substr(colon + 1); + key.erase(std::remove_if(key.begin(), key.end(), ::isspace), key.end()); + value.erase(0, value.find_first_not_of(" \t\r\n")); + value.erase(value.find_last_not_of(" \t\r\n") + 1); + req.headers[key] = value; + } + } + + size_t header_end = ctx.input_buffer.find("\r\n\r\n"); + if (header_end != std::string::npos) { + req.body = ctx.input_buffer.substr(header_end + 4); + if (req.headers.count("Content-Length")) { + size_t content_length = std::stoul(req.headers["Content-Length"]); + req.body = req.body.substr(0, content_length); + } + } +} + +void HttpWebhookServer::buildHttpResponse(const HttpResponse& res, std::string& output) { + output = "HTTP/1.1 " + std::to_string(res.status_code) + " OK\r\n"; + + for (const auto& [key, value] : res.headers) { + output += key + ": " + value + "\r\n"; + } + + if (!res.headers.count("Content-Length")) { + output += "Content-Length: " + std::to_string(res.body.size()) + "\r\n"; + } + + output += "\r\n" + res.body; +} diff --git a/bot/src/main.cpp b/bot/src/main.cpp index da38d43..557463e 100644 --- a/bot/src/main.cpp +++ b/bot/src/main.cpp @@ -2,6 +2,7 @@ #include <string> #include "../include/utils.hpp" #include "../include/http_webhook_server.hpp" +#include "../include/handle_actions.hpp" #include <thread> int main(int argc, char* argv[]) { @@ -18,22 +19,34 @@ int main(int argc, char* argv[]) { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([&json_data, &bot](const dpp::slashcommand_t& event) { + bot.on_slashcommand([&json_data, &bot](const dpp::slashcommand_t& event) -> dpp::task<void> { std::unordered_map<std::string, std::string> key_values = app::generate_key_values(event); std::string command_name = event.command.get_command_name(); std::string response = "Interaction found, but no response found."; if (!command_name.empty() && json_data->contains(command_name)) { auto& command_data = (*json_data)[command_name]; - bool no_error = true; - if (command_data.contains("action")) { - auto& action = command_data["action"]; + if (command_data.contains("actions")) { + auto& action = command_data["actions"]; // Actions are a list of Objects if (action.is_array()) { - no_error = app::handle_actions(event, action, key_values, bot); + std::cout << "Executing → Actions: " << action.dump() << std::endl; + auto already_returned_message = co_await handle_actions(event, action, key_values); + if(!already_returned_message) { + std::cout << "Command: " << command_name << " → Action: " << action.dump() << std::endl; + co_return; + }else { + // This mean we need to edit the response, not reply + if(command_data.contains("response")) { + response = command_data["response"]; + std::cout << "Command: " << command_name << " → Response: " << response << std::endl; + } + event.edit_response(app::update_string(response, key_values)); + co_return; + } } } - if (command_data.contains("response") && no_error) { + if (command_data.contains("response")) { response = command_data["response"]; std::cout << "Command: " << command_name << " → Response: " << response << std::endl; } diff --git a/bot/src/utils.cpp b/bot/src/utils.cpp new file mode 100644 index 0000000..349f584 --- /dev/null +++ b/bot/src/utils.cpp @@ -0,0 +1,215 @@ +#include <dpp/dpp.h> +#include <dpp/nlohmann/json.hpp> +#include <map> +#include <string> +#include <regex> +#include <sstream> +#include <algorithm> + +using namespace dpp; +namespace app +{ + // Helpers + std::string make_avatar_url(const user &u) + { + return u.avatar.to_string().empty() ? u.get_default_avatar_url() : u.get_avatar_url(1024, i_webp, true); + } + + std::string make_guild_icon(const guild &g) + { + return g.get_icon_url(1024, i_webp); + } + + std::string update_string(const std::string &initial, const std::unordered_map<std::string, std::string> &updates) + { + static const std::regex placeholderRegex(R"(\(\((.*?)\)\))", std::regex::icase); + + std::string result; + std::sregex_iterator it(initial.begin(), initial.end(), placeholderRegex); + std::sregex_iterator end; + + size_t last_pos = 0; + for (; it != end; ++it) + { + const auto &match = *it; + result.append(initial, last_pos, match.position() - last_pos); + + std::string content = match[1].str(); + std::vector<std::string> keys; + std::stringstream ss(content); + std::string key; + bool replaced = false; + + while (std::getline(ss, key, '|')) + { + key = trim(key); + auto found = updates.find(key); + if (found != updates.end()) + { + result.append(found->second); + replaced = true; + break; + } + } + if (!replaced) + { + // Aucune clé trouvée : chaîne vide + } + + last_pos = match.position() + match.length(); + } + result.append(initial, last_pos, std::string::npos); + return result; + } + // Forward declaration + void process_interaction_option(const slashcommand_t &event, const command_data_option &option, std::unordered_map<std::string, std::string> &kv); + + // Génère la map clé/valeur + std::unordered_map<std::string, std::string> generate_key_values(const slashcommand_t &event) + { + std::unordered_map<std::string, std::string> key_values; + const guild *g = event.command.is_guild_interaction() ? &event.command.get_guild() : nullptr; + const channel *channel_ptr = event.command.is_guild_interaction() ? &event.command.get_channel() : nullptr; + const user &u = event.command.get_issuing_user(); + key_values["commandName"] = event.command.get_command_name(); + key_values["commandId"] = event.command.id.str(); + key_values["commandType"] = std::to_string(event.command.type); + key_values["userName"] = u.username; + key_values["userId"] = u.id.str(); + key_values["userAvatar"] = make_avatar_url(u); + key_values["guildName"] = g ? g->name : "DM"; + key_values["channelName"] = channel_ptr ? channel_ptr->name : "DM"; + key_values["channelId"] = channel_ptr ? channel_ptr->id.str() : "0"; + key_values["channelType"] = channel_ptr ? std::to_string(channel_ptr->get_type()) : "0"; + key_values["guildId"] = g ? g->id.str() : "0"; + key_values["guildIcon"] = g ? make_guild_icon(*g) : ""; + key_values["guildCount"] = g ? std::to_string(g->member_count) : "0"; + key_values["guildOwner"] = g ? g->owner_id.str() : "0"; + key_values["guildCreatedAt"] = g ? std::to_string(g->get_creation_time()) : "0"; + key_values["guildBoostTier"] = g ? std::to_string(g->premium_tier) : "0"; + key_values["guildBoostCount"] = g ? std::to_string(g->premium_subscription_count) : "0"; + + // Options de commande + for (const auto &option : event.command.get_command_interaction().options) + { + process_interaction_option(event, option, key_values); + } + return key_values; + } + + // Traite une option d'interaction récursivement + void process_interaction_option(const slashcommand_t &event, const command_data_option &option, std::unordered_map<std::string, std::string> &kv) + { + switch (option.type) + { + case co_sub_command: + case co_sub_command_group: + for (const auto &subopt : option.options) + { + process_interaction_option(event, subopt, kv); + } + break; + case co_user: + { + snowflake user_id = std::get<snowflake>(option.value); + auto user_ptr = event.command.get_resolved_user(user_id); + const user &u = user_ptr; + kv["opts." + option.name] = u.username; + kv["opts." + option.name + ".id"] = u.id.str(); + kv["opts." + option.name + ".avatar"] = make_avatar_url(u); + kv["opts." + option.name + ".discriminator"] = std::to_string(u.discriminator); + kv["opts." + option.name + ".bot"] = u.is_bot() ? "true" : "false"; + kv["opts." + option.name + ".created_at"] = std::to_string(u.get_creation_time()); + } + break; + case co_channel: + { + snowflake chan_id = std::get<snowflake>(option.value); + auto chan_ptr = event.command.get_resolved_channel(chan_id); + const channel &c = chan_ptr; + kv["opts." + option.name] = c.name; + kv["opts." + option.name + ".id"] = c.id.str(); + kv["opts." + option.name + ".type"] = std::to_string(c.get_type()); + kv["opts." + option.name + ".created_at"] = std::to_string(c.get_creation_time()); + } + break; + case co_role: + { + snowflake role_id = std::get<snowflake>(option.value); + auto role_ptr = event.command.get_resolved_role(role_id); + const role &r = role_ptr; + kv["opts." + option.name] = r.name; + kv["opts." + option.name + ".id"] = r.id.str(); + kv["opts." + option.name + ".color"] = std::to_string(r.colour); + kv["opts." + option.name + ".hoist"] = r.is_hoisted() ? "true" : "false"; + kv["opts." + option.name + ".position"] = std::to_string(r.position); + } + break; + case co_mentionable: + { + snowflake mentionable_id = std::get<snowflake>(option.value); + auto member_ptr = event.command.get_resolved_member(mentionable_id); + const user &u = *member_ptr.get_user(); + kv["opts." + option.name] = u.username; + kv["opts." + option.name + ".id"] = u.id.str(); + kv["opts." + option.name + ".avatar"] = make_avatar_url(u); + kv["opts." + option.name + ".discriminator"] = std::to_string(u.discriminator); + kv["opts." + option.name + ".bot"] = u.is_bot() ? "true" : "false"; + kv["opts." + option.name + ".created_at"] = std::to_string(u.get_creation_time()); + kv["opts." + option.name + ".nick"] = member_ptr.get_nickname(); + kv["opts." + option.name + ".joined_at"] = std::to_string(member_ptr.joined_at); + } + break; + case co_string: + kv["opts." + option.name] = std::get<std::string>(option.value); + break; + case co_integer: + kv["opts." + option.name] = std::to_string(std::get<int64_t>(option.value)); + break; + case co_boolean: + kv["opts." + option.name] = std::get<bool>(option.value) ? "true" : "false"; + break; + case co_number: + kv["opts." + option.name] = std::to_string(std::get<double>(option.value)); + break; + case co_attachment: + { + snowflake attachment_id = std::get<snowflake>(option.value); + auto att_ptr = event.command.get_resolved_attachment(attachment_id); + kv["opts." + option.name] = att_ptr.url; + kv["opts." + option.name + ".id"] = att_ptr.id.str(); + kv["opts." + option.name + ".filename"] = att_ptr.filename; + kv["opts." + option.name + ".size"] = std::to_string(att_ptr.size); + } + break; + } + } + + nlohmann::json json_from_string(const std::string &str) + { + nlohmann::json j; + try + { + j = nlohmann::json::parse(str); + } + catch (const nlohmann::json::parse_error &e) + { + std::cerr << "JSON parse error: " << e.what() << std::endl; + } + return j; + } + + std::string string_from_json(const nlohmann::json &j) + { + std::string str; + try + { + str = j.dump(); + } + catch (const nlohmann::json::exception &e) + { + std::cerr << "JSON exception: " << e.what() << std::endl; + } + return str; + } +} |
