diff options
| -rw-r--r-- | app/cmd/main.go | 57 | ||||
| -rw-r--r-- | app/internal/create_bot.go | 61 | ||||
| -rw-r--r-- | bot/CMakeLists.txt | 10 | ||||
| -rw-r--r-- | bot/include/server.cpp | 182 | ||||
| -rw-r--r-- | bot/include/utils.cpp | 38 | ||||
| -rw-r--r-- | bot/include/utils.hpp | 22 | ||||
| -rw-r--r-- | bot/src/main.cpp | 81 |
7 files changed, 421 insertions, 30 deletions
diff --git a/app/cmd/main.go b/app/cmd/main.go index c13de97..604f66f 100644 --- a/app/cmd/main.go +++ b/app/cmd/main.go @@ -1,7 +1,14 @@ package main import ( + "encoding/json" + "fmt" + "log" "net/http" + "os" + "os/signal" + + "github.com/ketsuna-org/bot-creator-api/internal" ) func init() { @@ -15,6 +22,56 @@ func main() { mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, World!")) }) + botId := "XXXXX" + botToken := "XXXXX" + + bot := &internal.Bot{ + BotID: botId, + BotToken: botToken, + } + conn, err := internal.Start(bot) + if err != nil { + log.Fatalf("Error starting bot: %v", err) + } + defer conn.Close() + // Handle the bot connection + data, err := json.Marshal(map[string]interface{}{ + "command": "update", + "data": map[string]interface{}{ + "ping": map[string]string{ + "response": "pong ((userName))", + }, + }, + }) + if err != nil { + log.Fatalf("Error marshaling JSON: %v", err) + } + conn.Write(data) + + // Handle if signal is received + signals := make(chan os.Signal, 1) + signal.Notify(signals, os.Interrupt) + signal.Notify(signals, os.Kill) + go func() { + sig := <-signals + log.Printf("Received signal: %s", sig) + // let's kill the bot + if bot.Cmd != nil { + if err := bot.Cmd.Process.Kill(); err != nil { + log.Printf("Error killing bot process: %v", err) + } else { + log.Printf("Bot process killed successfully") + } + } + // let's remove the socket + socketPath := fmt.Sprintf("/tmp/%s.sock", bot.BotID) + if err := os.RemoveAll(socketPath); err != nil { + log.Printf("Error removing socket: %v", err) + } else { + log.Printf("Socket removed successfully") + } + os.Exit(0) + }() panic(http.ListenAndServe(":2030", mux)) } diff --git a/app/internal/create_bot.go b/app/internal/create_bot.go new file mode 100644 index 0000000..c1bea85 --- /dev/null +++ b/app/internal/create_bot.go @@ -0,0 +1,61 @@ +package internal + +import ( + "fmt" + "net" + "os" + "os/exec" + "syscall" + "time" +) + +type Bot struct { + BotID string `json:"bot_id"` + BotToken string `json:"bot_token"` + Cmd *exec.Cmd // Ajouter une référence à la commande + processID int // Stocker le PGID (Process Group ID) +} + +func Start(b *Bot) (net.Conn, error) { + socketPath := fmt.Sprintf("/tmp/%s.sock", b.BotID) + + // Nettoyage préalable du socket + if err := os.RemoveAll(socketPath); err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("error cleaning socket: %w", err) + } + + // Configuration du bot + cmd := exec.Command("../bot/build/discord-bot", b.BotToken) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, // Permet de kill le processus enfant si nécessaire + } + + // Redirection des sorties pour le débogage + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start bot: %w", err) + } + b.Cmd = cmd + b.processID = cmd.Process.Pid + + // Mécanisme d'attente intelligente pour le socket + var conn net.Conn + maxRetries := 10 + for i := 0; i < maxRetries; i++ { + var err error + conn, err = net.Dial("unix", socketPath) + if err == nil { + break + } + time.Sleep(500 * time.Millisecond) + } + + if conn == nil { + return nil, fmt.Errorf("failed to connect to bot socket after %d attempts", maxRetries) + } + + fmt.Printf("Bot %s started successfully\n", b.BotID) + return conn, nil +} diff --git a/bot/CMakeLists.txt b/bot/CMakeLists.txt index 5fef1c3..2dc8fb1 100644 --- a/bot/CMakeLists.txt +++ b/bot/CMakeLists.txt @@ -6,10 +6,6 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -# Find required dependencies -find_package(OpenSSL REQUIRED) -find_package(ZLIB REQUIRED) - # Configure DPP library (assuming you have it installed via Nix) find_package(dpp REQUIRED HINTS ${DPP_INCLUDE_DIR} # Add path if needed: /nix/store/.../include @@ -29,17 +25,11 @@ add_executable(${PROJECT_NAME} # Link libraries with proper dependencies target_link_libraries(${PROJECT_NAME} PRIVATE dpp - OpenSSL::SSL - OpenSSL::Crypto - ZLIB::ZLIB - pthread # Required for DPP's threading ) # Include directories target_include_directories(${PROJECT_NAME} PRIVATE ${DPP_INCLUDE_DIRS} - ${OPENSSL_INCLUDE_DIR} - ${Sodium_INCLUDE_DIRS} ) # macOS ARM64 specific fixes diff --git a/bot/include/server.cpp b/bot/include/server.cpp new file mode 100644 index 0000000..55e5df9 --- /dev/null +++ b/bot/include/server.cpp @@ -0,0 +1,182 @@ +#include <iostream> +#include <thread> +#include <mutex> +#include <vector> +#include <queue> +#include <functional> +#include <sys/socket.h> +#include <sys/un.h> +#include <poll.h> +#include <fcntl.h> +#include <unistd.h> +#include <cstring> + +#ifdef __APPLE__ +#define SOCKET_PATH_MAX 104 +#else +#define SOCKET_PATH_MAX 108 +#endif + +class UnixSocketServer { +public: + using MessageHandler = std::function<void(const std::string&)>; + + UnixSocketServer(const std::string& path) : + socket_path_(path.substr(0, SOCKET_PATH_MAX - 1)), + running_(false) {} + + ~UnixSocketServer() { stop(); } + + void start(MessageHandler handler) { + if (running_) return; + + server_fd_ = create_socket(); + setup_socket(); + + running_ = true; + server_thread_ = std::thread(&UnixSocketServer::event_loop, this, handler); + } + + void stop() { + running_ = false; + if (server_thread_.joinable()) { + server_thread_.join(); + } + cleanup(); + } + + void send(const std::string& message) { + std::lock_guard<std::mutex> lock(clients_mutex_); + for (auto& client : clients_) { + queue_message(client.fd, message); + } + } + +private: + struct Client { + int fd; + std::queue<std::string> write_queue; + }; + + int create_socket() { + int fd = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) throw std::runtime_error("::socket() failed"); + return fd; + } + + void setup_socket() { + struct sockaddr_un addr = {}; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path_.c_str(), sizeof(addr.sun_path) - 1); + + unlink(socket_path_.c_str()); + + if (bind(server_fd_, (struct sockaddr*)&addr, sizeof(addr)) == -1) { + close(server_fd_); + throw std::runtime_error("bind() failed"); + } + + if (listen(server_fd_, 5) == -1) { + close(server_fd_); + throw std::runtime_error("listen() failed"); + } + + set_nonblocking(server_fd_); + } + + void set_nonblocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + } + + void event_loop(MessageHandler handler) { + std::vector<struct pollfd> fds; + fds.push_back({server_fd_, POLLIN, 0}); + + while (running_) { + int ready = ::poll(fds.data(), fds.size(), 250); + if (ready == -1) break; + + for (size_t i = 0; i < fds.size(); ++i) { + if (fds[i].revents & POLLIN) { + if (fds[i].fd == server_fd_) { + accept_new_connection(fds); + } else { + handle_client_input(fds[i].fd, handler); + } + } + + if (fds[i].revents & POLLOUT) { + handle_client_output(fds[i].fd); + } + } + } + } + + void accept_new_connection(std::vector<struct pollfd>& fds) { + int client_fd = accept(server_fd_, nullptr, nullptr); + if (client_fd == -1) return; + + set_nonblocking(client_fd); + fds.push_back({client_fd, POLLIN | POLLOUT, 0}); + + std::lock_guard<std::mutex> lock(clients_mutex_); + clients_.push_back({client_fd, {}}); + } + + void handle_client_input(int fd, MessageHandler handler) { + char buffer[4096]; + ssize_t count = recv(fd, buffer, sizeof(buffer), 0); + + if (count > 0) { + handler(std::string(buffer, count)); + } else { + remove_client(fd); + } + } + + void handle_client_output(int fd) { + std::lock_guard<std::mutex> lock(clients_mutex_); + for (auto& client : clients_) { + if (client.fd == fd && !client.write_queue.empty()) { + const std::string& msg = client.write_queue.front(); + ssize_t sent = ::send(fd, msg.data(), msg.size(), 0); + if (sent > 0) { + client.write_queue.pop(); + } + } + } + } + + void queue_message(int fd, const std::string& message) { + std::lock_guard<std::mutex> lock(clients_mutex_); + for (auto& client : clients_) { + if (client.fd == fd) { + client.write_queue.push(message); + return; + } + } + } + + void remove_client(int fd) { + std::lock_guard<std::mutex> lock(clients_mutex_); + clients_.erase( + std::remove_if(clients_.begin(), clients_.end(), + [fd](const Client& c) { return c.fd == fd; }), + clients_.end() + ); + close(fd); + } + + void cleanup() { + close(server_fd_); + unlink(socket_path_.c_str()); + } + + std::string socket_path_; + int server_fd_ = -1; + std::atomic<bool> running_; + std::thread server_thread_; + std::mutex clients_mutex_; + std::vector<Client> clients_; +}; diff --git a/bot/include/utils.cpp b/bot/include/utils.cpp index 6a47d42..081fd4e 100644 --- a/bot/include/utils.cpp +++ b/bot/include/utils.cpp @@ -20,7 +20,7 @@ namespace app return g.get_icon_url(1024, i_webp); } - std::string update_string(const std::string &initial, const std::map<std::string, std::string> &updates) + 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); @@ -62,12 +62,12 @@ namespace app return result; } // Forward declaration - void process_interaction_option(const slashcommand_t &event, const command_data_option &option, std::map<std::string, std::string> &kv); + 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::map<std::string, std::string> generate_key_values(const slashcommand_t &event) + std::unordered_map<std::string, std::string> generate_key_values(const slashcommand_t &event) { - std::map<std::string, std::string> key_values; + std::unordered_map<std::string, std::string> key_values; const guild *g = event.command.is_guild_interaction() ? &event.command.get_guild() : nullptr; const channel &c = event.command.get_channel(); const user &u = event.command.get_issuing_user(); @@ -98,7 +98,7 @@ namespace app } // Traite une option d'interaction récursivement - void process_interaction_option(const slashcommand_t &event, const command_data_option &option, std::map<std::string, std::string> &kv) + void process_interaction_option(const slashcommand_t &event, const command_data_option &option, std::unordered_map<std::string, std::string> &kv) { switch (option.type) { @@ -184,4 +184,32 @@ namespace app 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; + } } diff --git a/bot/include/utils.hpp b/bot/include/utils.hpp index 19c834c..57f4005 100644 --- a/bot/include/utils.hpp +++ b/bot/include/utils.hpp @@ -37,7 +37,7 @@ namespace app * @param updates A map of key-value pairs to replace placeholders * @return std::string The updated string with placeholders replaced */ - std::string update_string(const std::string &initial, const std::map<std::string, std::string> &updates); + std::string update_string(const std::string &initial, const std::unordered_map<std::string, std::string> &updates); /** * @brief Processes a command option recursively and adds values to the key-value map @@ -46,7 +46,7 @@ namespace app * @param option The command option to process * @param kv The key-value map to update */ - void process_interaction_option(const slashcommand_t &event, const command_data_option &option, std::map<std::string, std::string> &kv); + void process_interaction_option(const slashcommand_t &event, const command_data_option &option, std::unordered_map<std::string, std::string> &kv); /** * @brief Generates a map of key-value pairs from a slash command event @@ -54,7 +54,23 @@ namespace app * @param event The slash command event * @return std::map<std::string, std::string> A map containing information about the command, user, guild, and options */ - std::map<std::string, std::string> generate_key_values(const slashcommand_t &event); + std::unordered_map<std::string, std::string> generate_key_values(const slashcommand_t &event); + + /** + * @brief Parses a JSON string into a JSON object + * + * @param str The JSON string to parse + * @return nlohmann::json The parsed JSON object + */ + nlohmann::json json_from_string(const std::string &str); + + /** + * @brief Converts a JSON object into a string + * + * @param j The JSON object to convert + * @return std::string The string representation of the JSON object + */ + std::string string_from_json(const nlohmann::json &j); } // namespace dpp diff --git a/bot/src/main.cpp b/bot/src/main.cpp index 500170d..06a03b0 100644 --- a/bot/src/main.cpp +++ b/bot/src/main.cpp @@ -1,26 +1,83 @@ #include <dpp/dpp.h> +#include <string> #include "../include/utils.hpp" -const std::string BOT_TOKEN = getenv("BOT_TOKEN"); +#include "../include/server.cpp" -int main() { - dpp::cluster bot(BOT_TOKEN); +int main(int argc, char *argv[]) +{ + if (argc > 1) + { + std::string token = argv[1]; + setenv("BOT_TOKEN", token.c_str(), 1); + } + + const std::string BOT_TOKEN = getenv("BOT_TOKEN"); + dpp::cluster bot(BOT_TOKEN); + std::unique_ptr<UnixSocketServer> server; + std::unique_ptr<nlohmann::json> json_data = std::make_unique<nlohmann::json>(); bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([](const dpp::slashcommand_t& event) { + bot.on_slashcommand([&json_data](const dpp::slashcommand_t &event) + { // let's generate the key-value map - std::map<std::string, std::string> key_values = app::generate_key_values(event); + std::unordered_map<std::string, std::string> key_values = app::generate_key_values(event); // let's create a string to send - std::string response = "Pong! ((userName))"; - - if (event.command.get_command_name() == "ping") { - event.reply(app::update_string(response, key_values)); + std::string response = "Interaction found, but no response found."; + // let's first check if it's a command or not. + if(event.command.get_command_name() != ""){ + // let's check if the command is in the json_data + // display json_data as string in the console + if (json_data->contains(event.command.get_command_name())) + { + // let's check if it does exist + std::cout << "Command found: " << event.command.get_command_name() << std::endl; + auto command_data = json_data->at(event.command.get_command_name()); + if (command_data.contains("response")) + { + std::cout << "Response found: " << command_data.at("response") << std::endl; + response = command_data.at("response"); + }else + { + // let's display the command data + std::cout << "Command data: " << command_data.dump(4) << std::endl; + std::cout << "No response found for command: " << event.command.get_command_name() << std::endl; + } + } } + event.reply(app::update_string(response, key_values)); }); - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once<struct register_bot_commands>()) { - bot.global_command_create(dpp::slashcommand("ping", "Ping pong!", bot.me.id)); + bot.on_ready([&bot, &server, &json_data](const dpp::ready_t &event) + { + if (dpp::run_once<struct register_bot_commands>()) + { + // let's start the server + std::string socket_path = "/tmp/" + bot.me.id.str() + ".sock"; + server = std::make_unique<UnixSocketServer>(socket_path); // Création explicite + + server->start([&json_data, &server](const std::string& msg) + { + // Traitement du message reçu + nlohmann::json j = app::json_from_string(msg); + if (j.contains("command")) + { + std::string command = j["command"]; + // ... [traitement de la commande] + if (command == "update") + { + // ... [traitement de la commande update] + json_data = std::make_unique<nlohmann::json>(j["data"]); + } + else if (command == "stop") + { + server->stop(); + std::cout << "Server stopped." << std::endl; + exit(0); + } + // ... [gestion des messages entrants] + } + }); } }); |
