summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoler_j <soler_j@etna-alternance.net>2025-04-29 10:50:45 +0200
committersoler_j <soler_j@etna-alternance.net>2025-04-29 10:50:45 +0200
commit3d143c271394a942dff5d979ffc8d2c55f1644dc (patch)
tree7191a49baacebe7a8bbb46b82c8d8f1f96bf5f0e
parent22dc3abd0c866e4ee292a4648c3dac5cda2583cb (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--Dockerfile10
-rw-r--r--app/cmd/main.go32
-rw-r--r--app/go.mod5
-rw-r--r--app/internal/create_bot.go74
-rw-r--r--bot/include/http_webhook_server.cpp175
-rw-r--r--bot/include/http_webhook_server.hpp53
-rw-r--r--bot/src/main.cpp94
-rw-r--r--devenv.nix2
8 files changed, 323 insertions, 122 deletions
diff --git a/Dockerfile b/Dockerfile
index a8e9c9c..bfc5949 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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)
diff --git a/app/go.mod b/app/go.mod
index 86e0320..0fdbadd 100644
--- a/app/go.mod
+++ b/app/go.mod
@@ -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();
}
});
diff --git a/devenv.nix b/devenv.nix
index 6744be6..a553e5c 100644
--- a/devenv.nix
+++ b/devenv.nix
@@ -16,8 +16,6 @@
libopus
libsodium
pkg-config
- zeromq
- cppzmq
ninja
(stdenv.mkDerivation {
name = "dpp";