diff options
| author | soler_j <soler_j@etna-alternance.net> | 2025-04-29 10:50:45 +0200 |
|---|---|---|
| committer | soler_j <soler_j@etna-alternance.net> | 2025-04-29 10:50:45 +0200 |
| commit | 3d143c271394a942dff5d979ffc8d2c55f1644dc (patch) | |
| tree | 7191a49baacebe7a8bbb46b82c8d8f1f96bf5f0e | |
| parent | 22dc3abd0c866e4ee292a4648c3dac5cda2583cb (diff) | |
Refactor Dockerfile and bot code: remove ZeroMQ dependencies, enhance bot initialization with HTTP webhook server, and improve error handling for bot startup.
| -rw-r--r-- | Dockerfile | 10 | ||||
| -rw-r--r-- | app/cmd/main.go | 32 | ||||
| -rw-r--r-- | app/go.mod | 5 | ||||
| -rw-r--r-- | app/internal/create_bot.go | 74 | ||||
| -rw-r--r-- | bot/include/http_webhook_server.cpp | 175 | ||||
| -rw-r--r-- | bot/include/http_webhook_server.hpp | 53 | ||||
| -rw-r--r-- | bot/src/main.cpp | 94 | ||||
| -rw-r--r-- | devenv.nix | 2 |
8 files changed, 323 insertions, 122 deletions
@@ -2,12 +2,7 @@ FROM golang:1.23 AS go-builder WORKDIR /app COPY app/ . -RUN apt-get update && apt-get install -y \ - pkg-config \ - libczmq-dev \ - libzmq3-dev \ - libsodium-dev -RUN CGO_ENABLED=1 go build -o /api/app ./cmd/main.go +RUN CGO_ENABLED=0 go build -o /api/app ./cmd/main.go # Étape de compilation pour le programme C++ avec DPP FROM ubuntu:24.04 AS cpp-builder @@ -22,8 +17,6 @@ RUN apt-get update && apt-get install -y \ libopus-dev \ clang \ pkg-config \ - libczmq-dev \ - libzmq3-dev \ libsodium-dev # Clone DPP @@ -60,7 +53,6 @@ RUN apt-get update && apt-get install -y \ zlib1g \ libopus0 \ libsodium23 \ - libzmq5 \ && apt-get clean # Copie des binaires COPY --from=go-builder /api/app ./app diff --git a/app/cmd/main.go b/app/cmd/main.go index 49e645f..2232c8e 100644 --- a/app/cmd/main.go +++ b/app/cmd/main.go @@ -40,7 +40,37 @@ func main() { ProcessID: fmt.Sprint(len(botList) + 5555), // or any unique identifier } - bot, err := internal.Start(bot) + // Let's check if this discord bot exists + if _, ok := botList[botToken]; ok { + log.Printf("[SERVER] Bot already running: %s", botToken) + http.Error(w, "Bot already running", http.StatusConflict) + return + } + + // let's check if it exist on Discord + httpClient := &http.Client{} + req, err := http.NewRequest("GET", "https://discord.com/api/v10/users/@me", nil) + if err != nil { + log.Printf("[SERVER] Error creating request: %v", err) + http.Error(w, "Error creating request", http.StatusInternalServerError) + return + } + req.Header.Set("Authorization", "Bot "+botToken) + resp, err := httpClient.Do(req) + if err != nil { + log.Printf("[SERVER] Error sending request: %v", err) + http.Error(w, "Error sending request", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + log.Printf("[SERVER] Bot not found: %s", botToken) + http.Error(w, "Bot not found", http.StatusNotFound) + return + } + // let's check if the bot is already running + + bot, err = internal.Start(bot) if err != nil { log.Printf("[SERVER] Error starting bot: %v", err) http.Error(w, "Error starting bot", http.StatusInternalServerError) @@ -2,7 +2,4 @@ module github.com/ketsuna-org/bot-creator-api go 1.23.3 -require ( - github.com/arangodb/go-driver/v2 v2.1.3 // indirect - github.com/pebbe/zmq4 v1.3.1 // indirect -) +require github.com/arangodb/go-driver/v2 v2.1.3 // indirect diff --git a/app/internal/create_bot.go b/app/internal/create_bot.go index 667ac0a..ce70cdd 100644 --- a/app/internal/create_bot.go +++ b/app/internal/create_bot.go @@ -3,45 +3,22 @@ package internal import ( "fmt" "log" + "net/http" "os" "os/exec" + "strings" "syscall" - - zmq "github.com/pebbe/zmq4" ) type Bot struct { BotToken string `json:"bot_token"` Cmd *exec.Cmd // Ajouter une référence à la commande ProcessID string - dealer *zmq.Socket // Stocker le PGID (Process Group ID) - read bool - readyChan chan bool // Canal pour indiquer que le bot est prêt + client *http.Client } func Start(b *Bot) (*Bot, error) { - if b.readyChan == nil { - b.readyChan = make(chan bool) - } - // Create a new ZeroMQ socket specifically for this bot - ctx, err := zmq.NewContext() - if err != nil { - return nil, fmt.Errorf("[SERVER] failed to create context: %w", err) - } - - // Each bot gets its own dealer socket - dealer, err := ctx.NewSocket(zmq.REP) - if err != nil { - return nil, fmt.Errorf("[SERVER] failed to create dealer: %w", err) - } - - // Binding the socket to a specific address (may need to adjust the address based on your needs) - err = dealer.Bind(fmt.Sprintf("tcp://localhost:%s", b.ProcessID)) - if err != nil { - return nil, fmt.Errorf("[SERVER] failed to bind dealer: %w", err) - } - // Configuration du bot cmd := exec.Command("./discord-bot", b.BotToken, b.ProcessID) // Passer le port unique cmd.SysProcAttr = &syscall.SysProcAttr{ @@ -56,20 +33,11 @@ func Start(b *Bot) (*Bot, error) { return nil, fmt.Errorf("failed to start bot: %w", err) } b.Cmd = cmd - b.dealer = dealer - // Here we will receive messages from the bot in a separate goroutine - for { - msg, err := dealer.Recv(0) - if err != nil { - return nil, fmt.Errorf("[SERVER] failed to receive message: %w", err) - } - if msg == "ready" { - log.Printf("[SERVER] Bot is ready") - b.read = true - break - } - } + client := http.Client{} + // Send data to the bot + b.client = &client + log.Printf("[SERVER] Bot %s started successfully with PID %d", b.BotToken, cmd.Process.Pid) return b, nil } @@ -84,22 +52,6 @@ func (b *Bot) Stop() error { } func (b *Bot) SendMessage(message string) error { - if b.dealer == nil { - return fmt.Errorf("[SERVER] sender socket is not initialized") - } - - // Wait for the bot to be ready if it's not already - if !b.read { - log.Printf("[SERVER] Waiting for bot %s to be ready...", b.BotToken) - // this mean we should read the recv channel - msg, err := b.dealer.Recv(0) - if err != nil { - return fmt.Errorf("[SERVER] failed to receive message: %w", err) - } - log.Printf("[SERVER] Received message from bot %s: %s", b.BotToken, msg) - b.read = true - } - // Check if the bot process is still running if err := b.Cmd.Process.Signal(syscall.Signal(0)); err != nil { return fmt.Errorf("[SERVER] bot process is not running: %w", err) @@ -111,12 +63,16 @@ func (b *Bot) SendMessage(message string) error { } // Send the message - _, err := b.dealer.Send(message, 0) + resp, err := b.client.Post("http://localhost:"+b.ProcessID, "application/json", strings.NewReader(message)) if err != nil { - return fmt.Errorf("[SERVER] failed to send message to bot %s: %w", b.BotToken, err) + return fmt.Errorf("[SERVER] failed to send message: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("[SERVER] failed to send message: %s", resp.Status) } - log.Printf("[SERVER] Sent message to bot %s: %s", b.BotToken, message) + // Log the message sent + log.Printf("[SERVER] Message sent to bot %s: %s", b.BotToken, message) - b.read = false // Reset read state after sending the message return nil } diff --git a/bot/include/http_webhook_server.cpp b/bot/include/http_webhook_server.cpp new file mode 100644 index 0000000..84815b7 --- /dev/null +++ b/bot/include/http_webhook_server.cpp @@ -0,0 +1,175 @@ +#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/include/http_webhook_server.hpp b/bot/include/http_webhook_server.hpp new file mode 100644 index 0000000..f840a16 --- /dev/null +++ b/bot/include/http_webhook_server.hpp @@ -0,0 +1,53 @@ +#include <sys/epoll.h> +#include <netinet/in.h> +#include <functional> +#include <string> +#include <unordered_map> +#include <system_error> +#include <sstream> + +class HttpWebhookServer { +public: + struct HttpRequest { + std::string method; + std::string path; + std::unordered_map<std::string, std::string> headers; + std::string body; + }; + + struct HttpResponse { + int status_code = 200; + std::unordered_map<std::string, std::string> headers; + std::string body; + }; + + using Handler = std::function<HttpResponse(const HttpRequest&)>; + + HttpWebhookServer(uint16_t port, Handler handler); + ~HttpWebhookServer(); + + void start(); + void stop(); + +private: + struct ClientContext { + std::string input_buffer; + std::string output_buffer; + size_t bytes_written = 0; + }; + + void setupSocket(); + void setupEpoll(); + void handleEvent(struct epoll_event* event); + void handleClient(int fd); + void closeClient(int fd); + void parseHttpRequest(ClientContext& ctx, HttpRequest& req); + void buildHttpResponse(const HttpResponse& res, std::string& output); + + int server_fd = -1; + int epoll_fd = -1; + bool running = false; + uint16_t port; + Handler request_handler; + std::unordered_map<int, ClientContext> clients; +}; diff --git a/bot/src/main.cpp b/bot/src/main.cpp index ccb3cba..4418b6f 100644 --- a/bot/src/main.cpp +++ b/bot/src/main.cpp @@ -1,15 +1,13 @@ #include <dpp/dpp.h> #include <string> -#include <zmq.hpp> #include "../include/utils.hpp" +#include "../include/http_webhook_server.hpp" #include <thread> -int main(int argc, char *argv[]) { - if (argc > 1) { - std::string token = argv[1]; - std::string port = argv[2]; - setenv("BOT_TOKEN", token.c_str(), 1); - setenv("PORT", port.c_str(), 1); +int main(int argc, char* argv[]) { + if (argc > 2) { + setenv("BOT_TOKEN", argv[1], 1); + setenv("PORT", argv[2], 1); } const std::string BOT_TOKEN = getenv("BOT_TOKEN"); @@ -20,61 +18,63 @@ int main(int argc, char *argv[]) { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([&json_data](const dpp::slashcommand_t &event) { + bot.on_slashcommand([&json_data](const dpp::slashcommand_t& event) { 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 (event.command.get_command_name() != "") { - if (json_data->contains(event.command.get_command_name())) { - 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 { - std::cout << "Command data: " << command_data.dump(4) << std::endl; - std::cout << "No response found for command: " << event.command.get_command_name() << std::endl; - } + + if (!command_name.empty() && json_data->contains(command_name)) { + auto& command_data = (*json_data)[command_name]; + if (command_data.contains("response")) { + response = command_data["response"]; + std::cout << "Command: " << command_name << " → Response: " << response << std::endl; + } else { + std::cout << "No response found for command: " << command_name << std::endl; } } + event.reply(app::update_string(response, key_values)); }); - bot.on_ready([&bot, &json_data, &PORT](const dpp::ready_t &event) { + bot.on_ready([&bot, &json_data, &PORT](const dpp::ready_t& event) { if (dpp::run_once<struct register_bot_commands>()) { - // Lancer la boucle ZMQ dans un thread séparé - std::thread zmq_thread([&json_data, &PORT]() { - zmq::context_t ctx; - zmq::socket_t responder(ctx, zmq::socket_type::req); - responder.connect("tcp://localhost:" + PORT); - zmq::message_t ready_msg(5); - memcpy(ready_msg.data(), "ready", 5); - responder.send(ready_msg, zmq::send_flags::none); + std::thread http_thread([&json_data, &PORT]() { + try { + HttpWebhookServer server(std::stoi(PORT), [&json_data](const HttpWebhookServer::HttpRequest& req) { + HttpWebhookServer::HttpResponse res; + + if (req.method == "POST") { + res.status_code = 200; + res.headers["Content-Type"] = "application/json"; - while (true) { - zmq::message_t reply; - if (responder.recv(reply, zmq::recv_flags::none)) { - std::string json_str(static_cast<char*>(reply.data()), reply.size()); - try { - nlohmann::json j = app::json_from_string(json_str); - if (j.contains("command")) { - std::string command = j["command"]; - if (command == "update") { - json_data = std::make_unique<nlohmann::json>(j["data"]); + try { + nlohmann::json body_json = app::json_from_string(req.body); + res.body = R"({"received": "POST request received"})"; + + if (body_json.contains("command") && body_json["command"] == "update") { + json_data = std::make_unique<nlohmann::json>(body_json["data"]); } + } catch (const std::exception& e) { + res.status_code = 400; + res.body = std::string("{\"error\": \"") + e.what() + "\"}"; } - // Répondre de nouveau si nécessaire - zmq::message_t ping(4); - memcpy(ping.data(), "pong", 4); - responder.send(ping, zmq::send_flags::none); - } catch (const std::exception& e) { - std::cerr << "[BOT] Error parsing JSON: " << e.what() << std::endl; + } else { + res.status_code = 400; + res.headers["Content-Type"] = "text/plain"; + res.body = "Invalid request method."; } - } - } + return res; + }); + + std::cout << "[BOT] Webhook server running on port " << PORT << "..." << std::endl; + server.start(); + } catch (const std::exception& e) { + std::cerr << "[BOT] Server error: " << e.what() << std::endl; + } }); - zmq_thread.detach(); // Le thread tourne en fond + http_thread.detach(); } }); @@ -16,8 +16,6 @@ libopus libsodium pkg-config - zeromq - cppzmq ninja (stdenv.mkDerivation { name = "dpp"; |
