commit b710fa3050267f0a0d2aebc6bb880a986233c6dd Author: Syping Date: Tue Jan 2 03:45:06 2024 +0100 initial commit diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1e42515 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,46 @@ +#[[************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 2023-2024 Syping +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* This software is provided as-is, no warranties are given to you, we are not +* responsible for anything with use of the software, you are self responsible. +****************************************************************************]] + +cmake_minimum_required(VERSION 3.16) +project(dtranslatebot VERSION 0.1 DESCRIPTION "Discord Translation Bot") + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +add_executable(${PROJECT_NAME} + src/main.cpp + src/queue.cpp + src/settings.cpp +) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(DPP REQUIRED) +find_package(Threads REQUIRED) + +target_link_libraries(${PROJECT_NAME} + ${DPP_LIBRARIES} + Threads::Threads +) + +target_include_directories(${PROJECT_NAME} PRIVATE + ${DPP_INCLUDE_DIR} +) + +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON +) diff --git a/cmake/FindDPP.cmake b/cmake/FindDPP.cmake new file mode 100644 index 0000000..a555a7f --- /dev/null +++ b/cmake/FindDPP.cmake @@ -0,0 +1,4 @@ +find_path(DPP_INCLUDE_DIR NAMES dpp/dpp.h HINTS ${DPP_ROOT_DIR}) +find_library(DPP_LIBRARIES NAMES dpp "libdpp.a" HINTS ${DPP_ROOT_DIR}) +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(DPP DEFAULT_MSG DPP_LIBRARIES DPP_INCLUDE_DIR) diff --git a/etc/dtranslatebot.json b/etc/dtranslatebot.json new file mode 100644 index 0000000..f57284d --- /dev/null +++ b/etc/dtranslatebot.json @@ -0,0 +1,24 @@ +{ + "token": "bot_token", + "guilds": { + "guild_1": { + "channel_1": { + "source": "en", + "target": "de", + "webhook": "https://..." + }, + "channel_2": { + "source": "de", + "target": "en", + "webhook": "https://..." + } + } + }, + "translate": { + "hostname": "127.0.0.1", + "port": 80, + "url": "/translate", + "tls": false, + "apiKey": "" + } +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..2b0b55d --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,67 @@ +/***************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 2023-2024 Syping +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* This software is provided as-is, no warranties are given to you, we are not +* responsible for anything with use of the software, you are self responsible. +*****************************************************************************/ + +#include +#include +#include +#include "queue.h" +#include "settings.h" + +int main(int argc, char* argv[]) { + if (argc != 2) { + std::cout << "Usage: " << argv[0] << " [json]" << std::endl; + return 0; + } + + bot::settings::settings settings; + if (!settings.parse(argv[1])) + return 0; + + dpp::cluster bot(settings.get_token(), dpp::i_default_intents | dpp::i_message_content); + + bot.on_log(dpp::utility::cout_logger()); + + bot::queue queue; + std::thread queue_loop(&bot::queue::run, &queue, &bot, &settings); + + bot.on_message_create([&bot, &queue, &settings](const dpp::message_create_t &event) { + if (event.msg.author.is_bot()) + return; + + settings.lock(); + bot::settings::guild *guild = settings.get_guild(event.msg.guild_id); + if (guild) { + bot::settings::channel *channel = settings.get_channel(guild, event.msg.channel_id); + if (channel) { + bot::message message; + message.author = event.msg.author.format_username(); + message.avatar = event.msg.author.avatar.to_string(); + message.message = event.msg.content; + message.webhook = channel->webhook; + message.source = channel->source; + message.target = channel->target; + queue.add(message); + } + } + settings.unlock(); + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/src/queue.cpp b/src/queue.cpp new file mode 100644 index 0000000..3ab5e71 --- /dev/null +++ b/src/queue.cpp @@ -0,0 +1,91 @@ +/***************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 2023-2024 Syping +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* This software is provided as-is, no warranties are given to you, we are not +* responsible for anything with use of the software, you are self responsible. +*****************************************************************************/ + +#include +#include +#include "queue.h" +#include "settings.h" +using namespace std::chrono_literals; + +void bot::queue::add(const bot::message &message) +{ + m_mutex.lock(); + m_queue.push_back(message); + m_mutex.unlock(); +} + +void bot::queue::run(dpp::cluster *bot, bot::settings::settings *settings) +{ + while (true) { + m_mutex.lock(); + if (!m_queue.empty()) { + const bot::message message = m_queue.front(); + m_queue.erase(m_queue.begin()); + m_mutex.unlock(); + + settings->lock(); + bot::settings::translate *translate = settings->get_translate(); + + dpp::json body = { + {"q", message.message}, + {"source", message.source}, + {"target", message.target}, + {"format", "text"}, + }; + + if (!translate->apiKey.empty()) + body.emplace("apiKey", translate->apiKey); + + const std::string tr_hostname = translate->hostname; + const uint16_t tr_port = translate->port; + const std::string tr_url = translate->url; + const bool tr_tls = translate->tls; + settings->unlock(); + + dpp::http_headers http_headers; + http_headers.emplace("Content-Type", "application/json"); + + std::string tr_message = message.message; + dpp::https_client http_request(tr_hostname, tr_port, tr_url, "POST", body.dump(), http_headers, !tr_tls); + if (http_request.get_status() == 200) { + dpp::json response = dpp::json::parse(http_request.get_content()); + if (response.is_object()) { + auto tr_text = response.find("translatedText"); + if (tr_text != response.end()) + tr_message = tr_text.value(); + } + } + + dpp::webhook webhook(message.webhook); + webhook.name = message.author; + + try { + bot->execute_webhook_sync(webhook, dpp::message(tr_message)); + } + catch (dpp::rest_exception exception) { + std::cerr << "REST Error: " << exception.what() << std::endl; + } + + std::this_thread::yield(); + } + else { + m_mutex.unlock(); + std::this_thread::sleep_for(100ms); + } + } +} diff --git a/src/queue.h b/src/queue.h new file mode 100644 index 0000000..07b2fa8 --- /dev/null +++ b/src/queue.h @@ -0,0 +1,50 @@ +/***************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 2023-2024 Syping +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* This software is provided as-is, no warranties are given to you, we are not +* responsible for anything with use of the software, you are self responsible. +*****************************************************************************/ + +#ifndef QUEUE_H +#define QUEUE_H +#include +#include +#include +#include +#include "settings.h" + +namespace bot { + struct message { + std::string author; + std::string avatar; + std::string message; + /* Webhook URL */ + std::string webhook; + /* Translation Parameters */ + std::string source; + std::string target; + }; + + class queue { + public: + void add(const bot::message &message); + void run(dpp::cluster *bot, bot::settings::settings *settings); + + private: + std::mutex m_mutex; + std::vector m_queue; + }; +} + +#endif //QUEUE_H diff --git a/src/settings.cpp b/src/settings.cpp new file mode 100644 index 0000000..d15b2f7 --- /dev/null +++ b/src/settings.cpp @@ -0,0 +1,162 @@ +/***************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 2023-2024 Syping +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* This software is provided as-is, no warranties are given to you, we are not +* responsible for anything with use of the software, you are self responsible. +*****************************************************************************/ + +#include +#include +#include "settings.h" + +bot::settings::channel* bot::settings::settings::get_channel(bot::settings::guild *guild, uint64_t channel_id) +{ + for (auto channel = guild->channel.begin(); channel != guild->channel.end(); channel++) { + if (channel->id == channel_id) + return &(*channel); + } + return nullptr; +} + +bot::settings::guild* bot::settings::settings::get_guild(uint64_t guild_id) +{ + for (auto guild = m_guilds.begin(); guild != m_guilds.end(); guild++) { + if (guild->id == guild_id) + return &(*guild); + } + return nullptr; +} + +bot::settings::translate* bot::settings::settings::get_translate() +{ + return &m_translate_settings; +} + +const std::string bot::settings::settings::get_token() +{ + return m_token; +} + +void bot::settings::settings::lock() +{ + m_mutex.lock(); +} + +bool bot::settings::settings::parse(const std::string &filename) +{ + std::ifstream ifs(filename, std::ios::in | std::ios::binary); + if (!ifs.is_open()) { + std::cerr << "Failed to open JSON configuration file located at " << filename << std::endl; + return false; + } + + std::string sdata(std::istreambuf_iterator{ifs}, {}); + ifs.close(); + + dpp::json json = dpp::json::parse(sdata); + if (!json.is_object()) { + std::cerr << "JSON configuration file is corrupt" << std::endl; + return false; + } + + auto json_token = json.find("token"); + if (json_token == json.end()) { + std::cerr << "Bot token can not be found" << std::endl; + return false; + } + + m_mutex.lock(); + m_token = json_token.value(); + + auto json_translate = json.find("translate"); + if (json_translate == json.end()) { + std::cerr << "Translate settings can not be found" << std::endl; + m_mutex.unlock(); + return false; + } + + auto json_translate_hostname = json_translate.value().find("hostname"); + if (json_translate_hostname == json_translate.value().end()) { + std::cerr << "\"hostname\" can not be found in Translate settings" << std::endl; + m_mutex.unlock(); + return false; + } + m_translate_settings.hostname = json_translate_hostname.value(); + + auto json_translate_port = json_translate.value().find("port"); + if (json_translate_port == json_translate.value().end()) { + std::cerr << "\"port\" can not be found in Translate settings" << std::endl; + m_mutex.unlock(); + return false; + } + m_translate_settings.port = json_translate_port.value(); + + auto json_translate_url = json_translate.value().find("url"); + if (json_translate_url == json_translate.value().end()) { + std::cerr << "\"url\" can not be found in Translate settings" << std::endl; + m_mutex.unlock(); + return false; + } + m_translate_settings.url = json_translate_url.value(); + + auto json_translate_tls = json_translate.value().find("tls"); + if (json_translate_tls != json_translate.value().end()) + m_translate_settings.tls = json_translate_tls.value(); + else + m_translate_settings.tls = false; + + auto json_translate_apiKey = json_translate.value().find("apiKey"); + if (json_translate_apiKey != json_translate.value().end()) + m_translate_settings.apiKey = json_translate_apiKey.value(); + else + m_translate_settings.apiKey.clear(); + + auto json_guilds = json.find("guilds"); + if (json_guilds != json.end()) { + for (auto json_guild = json_guilds.value().begin(); json_guild != json_guilds.value().end(); json_guild++) { + if (json_guild.value().is_object()) { + bot::settings::guild guild; + guild.id = std::stoull(json_guild.key()); + for (auto json_channel = json_guild.value().begin(); json_channel != json_guild.value().end(); json_channel++) { + bot::settings::channel channel; + channel.id = std::stoull(json_channel.key()); + + auto json_channel_source = json_channel.value().find("source"); + if (json_channel_source != json_channel.value().end()) + channel.source = json_channel_source.value(); + + auto json_channel_target = json_channel.value().find("target"); + if (json_channel_target != json_channel.value().end()) + channel.target = json_channel_target.value(); + + auto json_channel_webhook = json_channel.value().find("webhook"); + if (json_channel_webhook != json_channel.value().end()) + channel.webhook = json_channel_webhook.value(); + + if (!channel.source.empty() && !channel.target.empty() && !channel.webhook.empty()) + guild.channel.push_back(channel); + } + m_guilds.push_back(guild); + } + } + } + + m_mutex.unlock(); + return true; +} + +void bot::settings::settings::unlock() +{ + m_mutex.unlock(); +} diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 0000000..34c0802 --- /dev/null +++ b/src/settings.h @@ -0,0 +1,64 @@ +/***************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 2023-2024 Syping +* +* Redistribution and use in source and binary forms, with or without modification, +* are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* This software is provided as-is, no warranties are given to you, we are not +* responsible for anything with use of the software, you are self responsible. +*****************************************************************************/ + +#ifndef SETTINGS_H +#define SETTINGS_H +#include +#include +#include + +namespace bot { + namespace settings { + struct channel { + uint64_t id; + std::string source; + std::string target; + std::string webhook; + }; + struct guild { + uint64_t id; + std::vector channel; + }; + struct translate { + std::string hostname; + uint16_t port; + std::string url; + bool tls; + std::string apiKey; + }; + + class settings { + public: + bot::settings::channel* get_channel(bot::settings::guild *guild, uint64_t channel_id); + bot::settings::guild* get_guild(uint64_t guild_id); + bot::settings::translate* get_translate(); + const std::string get_token(); + void lock(); + bool parse(const std::string &filename); + void unlock(); + + private: + std::recursive_mutex m_mutex; + std::vector m_guilds; + bot::settings::translate m_translate_settings; + std::string m_token; + }; + } +} + +#endif //SETTINGS_H