diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c460c6..1e29d92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,8 @@ set(DTRANSLATEBOT_HEADERS src/message_queue.h src/settings.h src/submit_queue.h + src/translate_core.h + src/translate_libretranslate.h src/webhook_push.h ) set(DTRANSLATEBOT_SOURCES @@ -32,6 +34,8 @@ set(DTRANSLATEBOT_SOURCES src/message_queue.cpp src/settings.cpp src/submit_queue.cpp + src/translate_core.cpp + src/translate_libretranslate.cpp src/webhook_push.cpp ) diff --git a/etc/dtranslatebot.json b/etc/dtranslatebot.json index 78e8590..81cdc1a 100644 --- a/etc/dtranslatebot.json +++ b/etc/dtranslatebot.json @@ -26,11 +26,12 @@ } } }, + "preferred_lang": ["en", "de", "fr", "ru"], "token": "$bot_token", "translate": { "hostname": "127.0.0.1", "port": 80, - "url": "/translate", + "url": "/", "tls": false, "apiKey": "" } diff --git a/src/main.cpp b/src/main.cpp index 2cfc21d..b79ce72 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include "message_queue.h" #include "settings.h" @@ -30,7 +31,17 @@ int main(int argc, char* argv[]) { bot::settings::settings settings; if (!settings.parse(argv[1])) - return 0; + return 1; + + { + std::vector languages; + languages = settings.get_translator()->get_languages(); + + if (languages.empty()) { + std::cerr << "Failed to initialise translateable languages" << std::endl; + return 2; + } + } dpp::cluster bot(settings.get_token(), dpp::i_default_intents | dpp::i_message_content); @@ -51,6 +62,10 @@ int main(int argc, char* argv[]) { return; } + // Same as before, just without the involvement of webhooks + if (event.msg.author.id == bot.me.id) + return; + const std::lock_guard guard(settings); const bot::settings::channel *channel = settings.get_channel(event.msg.guild_id, event.msg.channel_id); if (channel) { @@ -72,6 +87,162 @@ int main(int argc, char* argv[]) { } }); + bot.on_slashcommand([&bot, &settings](const dpp::slashcommand_t &event) { + if (event.command.get_command_name() == "translate" || event.command.get_command_name() == "translate_pref") { + try { + std::variant v_target; + const std::string source = std::get(event.get_parameter("source")); + const std::string target = std::get(event.get_parameter("target")); + + dpp::command_interaction interaction = event.command.get_command_interaction(); + if (interaction.options[0].name == "channel") { + v_target = event.command.get_resolved_channel( + std::get(event.get_parameter("channel"))); + } + else if (interaction.options[0].name == "webhook") { + v_target = dpp::webhook(std::get(event.get_parameter("webhook"))); + } + + const std::vector languages = settings.get_translator()->get_languages(); + + std::ostringstream language_codes; + bool source_valid = false, target_valid = false; + for (const bot::translate::language &language : languages) { + if (language.code == source) + source_valid = true; + if (language.code == target) + target_valid = true; + if (source_valid && target_valid) + break; + language_codes << " " << language.code; + } + + if (source_valid && target_valid) { + const std::lock_guard guard(settings); + if (!settings.get_channel(event.command.guild_id, event.command.channel_id)) { + if (dpp::channel *channel = std::get_if(&v_target)) { + dpp::webhook webhook; + webhook.channel_id = channel->id; + webhook.guild_id = channel->guild_id; + webhook.name = "Translate Bot Webhook <" + std::to_string(event.command.channel_id) + ":" + source + ":" + target + ">"; + + bot.create_webhook(webhook, [&bot, &settings, event, source, target](const dpp::confirmation_callback_t &callback) { + if (callback.is_error()) { + event.reply(dpp::message("Failed to generate webhook!\n" + callback.http_info.body).set_flags(dpp::m_ephemeral)); + return; + } + const dpp::webhook webhook = callback.get(); + + bot::settings::channel s_channel; + s_channel.id = event.command.channel_id; + s_channel.source = source; + + bot::settings::target s_target; + s_target.target = target; + s_target.webhook = webhook; + s_channel.targets.push_back(s_target); + + const std::lock_guard guard(settings); + settings.add_channel(s_channel, event.command.guild_id); + + event.reply(dpp::message("Channel will be now translated!").set_flags(dpp::m_ephemeral)); + }); + } + else if (dpp::webhook *webhook = std::get_if(&v_target)) { + bot::settings::channel s_channel; + s_channel.id = event.command.channel_id; + s_channel.source = source; + + bot::settings::target s_target; + s_target.target = target; + s_target.webhook = *webhook; + s_channel.targets.push_back(s_target); + + const std::lock_guard guard(settings); + settings.add_channel(s_channel, event.command.guild_id); + + event.reply(dpp::message("Channel will be now translated!").set_flags(dpp::m_ephemeral)); + } + } + else { + event.reply(dpp::message("The current channel is already being translated!").set_flags(dpp::m_ephemeral)); + } + } + else if (!source_valid && !target_valid) { + event.reply(dpp::message("Source and target languages are not valid!\nAvailable languages are:" + language_codes.str()).set_flags(dpp::m_ephemeral)); + } + else if (!source_valid) { + event.reply(dpp::message("Source language is not valid!\nAvailable languages are:" + language_codes.str()).set_flags(dpp::m_ephemeral)); + } + else if (!target_valid) { + event.reply(dpp::message("Target language is not valid!\nAvailable languages are:" + language_codes.str()).set_flags(dpp::m_ephemeral)); + } + } + catch (const std::exception &exception) { + std::cerr << "Failed to process command /" << event.command.get_command_name() << ": " << exception.what() << std::endl; + event.reply(dpp::message("Failed to process command /" + event.command.get_command_name() + "\n" + exception.what()).set_flags(dpp::m_ephemeral)); + } + } + }); + + bot.on_ready([&bot, &settings](const dpp::ready_t &event) { + if (dpp::run_once()) { + settings.lock(); + const std::vector languages = settings.get_translator()->get_languages(); + const std::vector preferred_languages = settings.get_preferred_languages(); + settings.unlock(); + + std::vector commands; + + dpp::slashcommand command_translate("translate", "Translate current channel (ISO 639-1)", bot.me.id); + command_translate.set_default_permissions(dpp::p_manage_webhooks); + dpp::command_option channel_subcommand(dpp::co_sub_command, "channel", "Translate current channel to a channel (ISO 639-1)"); + dpp::command_option webhook_subcommand(dpp::co_sub_command, "webhook", "Translate current channel to a webhook (ISO 639-1)"); + dpp::command_option source_option(dpp::co_string, "source", "Source language", true); + source_option.set_max_length(2).set_min_length(2); + dpp::command_option target_option(dpp::co_string, "target", "Target language", true); + target_option.set_max_length(2).set_min_length(2); + dpp::command_option channel_option(dpp::co_channel, "channel", "Target channel", true); + channel_option.add_channel_type(dpp::CHANNEL_TEXT); + dpp::command_option webhook_option(dpp::co_string, "webhook", "Target webhook", true); + channel_subcommand.add_option(source_option); + channel_subcommand.add_option(target_option); + channel_subcommand.add_option(channel_option); + webhook_subcommand.add_option(source_option); + webhook_subcommand.add_option(target_option); + webhook_subcommand.add_option(webhook_option); + command_translate.add_option(channel_subcommand); + command_translate.add_option(webhook_subcommand); + commands.push_back(command_translate); + + if (preferred_languages.size() > 1) { + dpp::slashcommand command_translate_pref("translate_pref", "Translate current channel (Preferred languages)", bot.me.id); + command_translate_pref.set_default_permissions(dpp::p_manage_webhooks); + dpp::command_option channel_pref_subcommand(dpp::co_sub_command, "channel", "Translate current channel to a channel (Preferred languages)"); + dpp::command_option webhook_pref_subcommand(dpp::co_sub_command, "webhook", "Translate current channel to a webhook (Preferred languages)"); + dpp::command_option source_pref_option(dpp::co_string, "source", "Source language", true); + dpp::command_option target_pref_option(dpp::co_string, "target", "Target language", true); + for (const bot::translate::language &language : languages) { + if (std::find(preferred_languages.begin(), preferred_languages.end(), language.code) != preferred_languages.end()) { + source_pref_option.add_choice(dpp::command_option_choice(language.name, language.code)); + target_pref_option.add_choice(dpp::command_option_choice(language.name, language.code)); + } + } + channel_pref_subcommand.add_option(source_pref_option); + channel_pref_subcommand.add_option(target_pref_option); + channel_pref_subcommand.add_option(channel_option); + webhook_pref_subcommand.add_option(source_pref_option); + webhook_pref_subcommand.add_option(target_pref_option); + webhook_pref_subcommand.add_option(webhook_option); + command_translate_pref.add_option(channel_pref_subcommand); + command_translate_pref.add_option(webhook_pref_subcommand); + commands.push_back(command_translate_pref); + } + + bot.global_bulk_command_create(commands); + } + }); + bot.start(dpp::st_wait); // It's unneccessary, but we choose to exit clean anyway diff --git a/src/message_queue.cpp b/src/message_queue.cpp index 4501698..a956a67 100644 --- a/src/message_queue.cpp +++ b/src/message_queue.cpp @@ -48,53 +48,10 @@ void bot::message_queue::run(bot::settings::settings *settings, bot::submit_queu m_queue.erase(m_queue.begin()); m_mutex.unlock(); - settings->lock(); - const bot::settings::translate *translate = settings->get_translate(); - const std::string tr_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(); + std::unique_ptr translator = settings->get_translator(); for (auto target = message.targets.begin(); target != message.targets.end(); target++) { - dpp::json json_body = { - {"q", message.message}, - {"source", message.source}, - {"target", target->target}, - {"format", "text"} - }; - - if (!tr_apiKey.empty()) - json_body["apiKey"] = tr_apiKey; - - const dpp::http_headers http_headers = { - {"Content-Type", "application/json"} - }; - - std::string tr_message = message.message; - - try { - dpp::https_client http_request(tr_hostname, tr_port, tr_url, "POST", json_body.dump(), http_headers, !tr_tls); - if (http_request.get_status() == 200) { - const 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(); - } - } - } - catch (const dpp::json::exception &exception) { - std::cerr << "Exception thrown while parsing translated JSON: " << exception.what() << std::endl; - } - catch (const std::exception &exception) { - std::cerr << "Exception thrown while translating: " << exception.what() << std::endl; - } - catch (...) { - std::cerr << "Exception thrown while translating: unknown" << std::endl; - } - + const std::string tr_message = translator->translate(message.message, message.source, target->target); submit_queue->add(make_translated_message(message, tr_message, target->webhook)); } diff --git a/src/settings.cpp b/src/settings.cpp index 507e8d4..ba86c1c 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -20,13 +20,31 @@ #include #include #include "settings.h" +#include "translate_libretranslate.h" + +void bot::settings::settings::add_channel(const bot::settings::channel &channel, dpp::snowflake guild_id) +{ + for (auto guild = m_guilds.begin(); guild != m_guilds.end(); guild++) { + if (guild->id == guild_id) { + guild->channel.push_back(channel); + return; + } + } + + // We will create the guild structure when it is not in memory + bot::settings::guild guild; + guild.id = guild_id; + guild.channel.push_back(channel); + m_guilds.push_back(guild); + return; +} uint16_t bot::settings::settings::get_avatar_size() { return m_avatarSize; } -const bot::settings::channel *bot::settings::settings::get_channel(const bot::settings::guild *guild, dpp::snowflake channel_id) +const bot::settings::channel* bot::settings::settings::get_channel(const bot::settings::guild *guild, dpp::snowflake channel_id) { for (auto channel = guild->channel.begin(); channel != guild->channel.end(); channel++) { if (channel->id == channel_id) @@ -35,7 +53,7 @@ const bot::settings::channel *bot::settings::settings::get_channel(const bot::se return nullptr; } -const bot::settings::channel *bot::settings::settings::get_channel(dpp::snowflake guild_id, dpp::snowflake channel_id) +const bot::settings::channel* bot::settings::settings::get_channel(dpp::snowflake guild_id, dpp::snowflake channel_id) { for (auto guild = m_guilds.begin(); guild != m_guilds.end(); guild++) { if (guild->id == guild_id) { @@ -48,7 +66,7 @@ const bot::settings::channel *bot::settings::settings::get_channel(dpp::snowflak return nullptr; } -const bot::settings::guild *bot::settings::settings::get_guild(dpp::snowflake guild_id) +const bot::settings::guild* bot::settings::settings::get_guild(dpp::snowflake guild_id) { for (auto guild = m_guilds.begin(); guild != m_guilds.end(); guild++) { if (guild->id == guild_id) @@ -57,11 +75,24 @@ const bot::settings::guild *bot::settings::settings::get_guild(dpp::snowflake gu return nullptr; } -const bot::settings::translate *bot::settings::settings::get_translate() +const std::vector bot::settings::settings::get_preferred_languages() +{ + return m_preflangs; +} + +const bot::settings::translate* bot::settings::settings::get_translate() { return &m_translate; } +std::unique_ptr bot::settings::settings::get_translator() +{ + const std::lock_guard guard(m_mutex); + std::unique_ptr libretranslate( + new bot::translate::libretranslate(m_translate.hostname, m_translate.port, m_translate.url, m_translate.tls, m_translate.apiKey)); + return libretranslate; +} + const std::string bot::settings::settings::get_token() { return m_token; @@ -164,6 +195,7 @@ bool bot::settings::settings::parse(const std::string &filename) m_avatarSize = 256; m_guilds.clear(); + m_preflangs.clear(); m_webhookIds.clear(); auto json_guilds = json.find("guilds"); @@ -233,6 +265,19 @@ bool bot::settings::settings::parse(const std::string &filename) } } + auto json_preflangs = json.find("preferred_lang"); + if (json_preflangs != json.end() && json_preflangs->is_array()) { + size_t i = 0; + for (const auto &json_preflang : json_preflangs.value()) { + if (i >= 25) { + std::cerr << "\"preferred_lang\" is limited to 25 languages" << std::endl; + break; + } + m_preflangs.push_back(json_preflang); + i++; + } + } + return true; } catch (const dpp::json::exception &exception) { @@ -241,9 +286,6 @@ bool bot::settings::settings::parse(const std::string &filename) catch (const std::exception &exception) { std::cerr << "Exception thrown while parsing configuration: " << exception.what() << std::endl; } - catch (...) { - std::cerr << "Exception thrown while parsing configuration: unknown" << std::endl; - } return false; } diff --git a/src/settings.h b/src/settings.h index 48ae550..e239797 100644 --- a/src/settings.h +++ b/src/settings.h @@ -24,6 +24,7 @@ #include #include #include +#include "translate_core.h" namespace bot { namespace settings { @@ -50,11 +51,14 @@ namespace bot { class settings { public: + void add_channel(const bot::settings::channel &channel, dpp::snowflake guild_id); uint16_t get_avatar_size(); const bot::settings::channel* get_channel(const bot::settings::guild *guild, dpp::snowflake channel_id); const bot::settings::channel* get_channel(dpp::snowflake guild_id, dpp::snowflake channel_id); const bot::settings::guild* get_guild(dpp::snowflake guild_id); + const std::vector get_preferred_languages(); const bot::settings::translate* get_translate(); + std::unique_ptr get_translator(); const std::string get_token(); bool is_translatebot(dpp::snowflake webhook_id); void lock(); @@ -65,6 +69,7 @@ namespace bot { uint16_t m_avatarSize; std::recursive_mutex m_mutex; std::vector m_guilds; + std::vector m_preflangs; bot::settings::translate m_translate; std::string m_token; std::vector m_webhookIds; diff --git a/src/translate_core.cpp b/src/translate_core.cpp new file mode 100644 index 0000000..d859e75 --- /dev/null +++ b/src/translate_core.cpp @@ -0,0 +1,36 @@ +/***************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 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 "translate_core.h" + +bot::translate::translator::translator() +{ +} + +const std::vector bot::translate::translator::get_languages() +{ + std::cerr << "WARNING: translator::get_languages() have being called." << std::endl; + return {}; +} + +const std::string bot::translate::translator::translate(const std::string &text, const std::string &source, const std::string &target) +{ + std::cerr << "WARNING: translator:translate(const std::string&, const std::string&, const std::string&) have being called." << std::endl; + return {}; +} diff --git a/src/translate_core.h b/src/translate_core.h new file mode 100644 index 0000000..4bcb2ed --- /dev/null +++ b/src/translate_core.h @@ -0,0 +1,41 @@ +/***************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 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 TRANSLATE_CORE_H +#define TRANSLATE_CORE_H + +#include +#include + +namespace bot { + namespace translate { + struct language { + std::string code; + std::string name; + }; + + class translator { + public: + explicit translator(); + virtual const std::vector get_languages(); + virtual const std::string translate(const std::string &text, const std::string &source, const std::string &target); + }; + } +} + +#endif // TRANSLATE_CORE_H diff --git a/src/translate_libretranslate.cpp b/src/translate_libretranslate.cpp new file mode 100644 index 0000000..10d11d7 --- /dev/null +++ b/src/translate_libretranslate.cpp @@ -0,0 +1,101 @@ +/***************************************************************************** +* 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 "translate_libretranslate.h" + +bot::translate::libretranslate::libretranslate(const std::string &hostname, uint16_t port, const std::string &url, bool tls, const std::string apiKey) : + m_hostname(hostname), m_port(port), m_url(url), m_tls(tls), m_apiKey(apiKey) +{ +} + +const std::vector bot::translate::libretranslate::get_languages() +{ + std::vector languages; + + try { + dpp::https_client http_request(m_hostname, m_port, m_url + "languages", "GET", {}, {}, !m_tls); + if (http_request.get_status() == 200) { + const dpp::json response = dpp::json::parse(http_request.get_content()); + if (response.is_array()) { + for (auto json_language = response.begin(); json_language != response.end(); json_language++) { + if (json_language->is_object()) { + bot::translate::language language; + + auto json_lang_code = json_language.value().find("code"); + if (json_lang_code != json_language.value().end()) + language.code = json_lang_code.value(); + + auto json_lang_name = json_language.value().find("name"); + if (json_lang_name != json_language.value().end()) + language.name = json_lang_name.value(); + + if (!language.code.empty() && !language.name.empty()) + languages.push_back(language); + } + } + } + } + } + catch (const dpp::json::exception &exception) { + std::cerr << "Exception thrown while parsing supported languages JSON: " << exception.what() << std::endl; + } + catch (const std::exception &exception) { + std::cerr << "Exception thrown while getting supported languages: " << exception.what() << std::endl; + } + + return languages; +} + +const std::string bot::translate::libretranslate::translate(const std::string &text, const std::string &source, const std::string &target) +{ + const dpp::http_headers http_headers = { + {"Content-Type", "application/json"} + }; + + dpp::json json_body = { + {"q", text}, + {"source", source}, + {"target", target}, + {"format", "text"} + }; + + if (!m_apiKey.empty()) + json_body["apiKey"] = m_apiKey; + + try { + dpp::https_client http_request(m_hostname, m_port, m_url + "translate", "POST", json_body.dump(), http_headers, !m_tls); + if (http_request.get_status() == 200) { + const 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()) + return tr_text.value(); + } + } + } + catch (const dpp::json::exception &exception) { + std::cerr << "Exception thrown while parsing translated JSON: " << exception.what() << std::endl; + } + catch (const std::exception &exception) { + std::cerr << "Exception thrown while translating: " << exception.what() << std::endl; + } + + return text; +} diff --git a/src/translate_libretranslate.h b/src/translate_libretranslate.h new file mode 100644 index 0000000..6f82c9a --- /dev/null +++ b/src/translate_libretranslate.h @@ -0,0 +1,44 @@ +/***************************************************************************** +* 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 TRANSLATE_LIBRETRANSLATE_H +#define TRANSLATE_LIBRETRANSLATE_H + +#include +#include +#include "translate_core.h" + +namespace bot { + namespace translate { + class libretranslate : public translator { + public: + explicit libretranslate(const std::string &hostname, uint16_t port, const std::string &url, bool tls, const std::string apiKey = {}); + const std::vector get_languages(); + const std::string translate(const std::string &text, const std::string &source, const std::string &target); + + private: + std::string m_apiKey; + std::string m_hostname; + uint16_t m_port; + std::string m_url; + bool m_tls; + }; + } +} + +#endif // TRANSLATE_LIBRETRANSLATE_H