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] +                } +            });          }      });  | 
