summaryrefslogtreecommitdiff
path: root/bot/src
diff options
context:
space:
mode:
Diffstat (limited to 'bot/src')
-rw-r--r--bot/src/handle_actions.cpp136
-rw-r--r--bot/src/http_webhook_server.cpp175
-rw-r--r--bot/src/main.cpp25
-rw-r--r--bot/src/utils.cpp215
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;
+ }
+}