diff --git a/.github/workflows/linux-rpm.yml b/.github/workflows/linux-rpm.yml index b51f55d..e710a11 100644 --- a/.github/workflows/linux-rpm.yml +++ b/.github/workflows/linux-rpm.yml @@ -6,8 +6,6 @@ jobs: strategy: matrix: include: - - name: Enterprise Linux 7 - version: el7 - name: Enterprise Linux 8 version: el8 - name: Enterprise Linux 9 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index c0607fc..0000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Windows -on: push -jobs: - Release: - runs-on: windows-latest - env: - BUILD_TYPE: Release - defaults: - run: - shell: msys2 {0} - steps: - - name: Setup MSYS2 - uses: msys2/setup-msys2@v2 - with: - msystem: clang64 - update: true - install: >- - git - make - mingw-w64-clang-x86_64-clang - mingw-w64-clang-x86_64-cmake - mingw-w64-clang-x86_64-ninja - perl - - name: Cloning - uses: actions/checkout@v4 - - name: Configure CMake - run: cmake -B dtranslatebot-build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_EXE_LINKER_FLAGS="-static -lc++" -DWITH_DPP_STATIC_BUNDLE=TRUE -GNinja - - name: Download and build OpenSSL - run: cmake --build dtranslatebot-build --config ${{env.BUILD_TYPE}} --target OpenSSL - - name: Download and build zlib - run: cmake --build dtranslatebot-build --config ${{env.BUILD_TYPE}} --target ZLIB - - name: Download and build DPP - run: cmake --build dtranslatebot-build --config ${{env.BUILD_TYPE}} --target DPP - - name: Build dtranslatebot - run: cmake --build dtranslatebot-build --config ${{env.BUILD_TYPE}} - - name: Install - run: cmake --install dtranslatebot-build --config ${{env.BUILD_TYPE}} --prefix dtranslatebot-install --strip - - name: Upload - uses: actions/upload-artifact@v4 - with: - name: Windows - path: | - dtranslatebot-install/bin/dtranslatebot.exe diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fe0ab1..17fa1bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,8 @@ include(GNUInstallDirs) # dtranslatebot Source files set(DTRANSLATEBOT_HEADERS src/core/database.h + src/core/http_request.h + src/core/http_response.h src/core/message_queue.h src/core/regex.h src/core/settings.h @@ -41,6 +43,7 @@ set(DTRANSLATEBOT_HEADERS ) set(DTRANSLATEBOT_SOURCES src/core/database.cpp + src/core/http_request.cpp src/core/main.cpp src/core/message_queue.cpp src/core/settings.cpp @@ -59,6 +62,12 @@ set(DTRANSLATEBOT_SOURCES # dtranslatebot Module Path list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +# curl Library +find_package(CURL REQUIRED) +list(APPEND DTRANSLATEBOT_LIBRARIES + CURL::libcurl +) + # Boost C++ Libraries option(WITH_BOOST "Build with Boost C++ Libraries" OFF) if (WITH_BOOST) diff --git a/README.md b/README.md index 0e3049c..8e68dab 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Open Source Discord Translation Bot #### Build Dependencies - CMake 3.16 or newer - Compiler with C++17 Support +- curl - [D++: A C++ Discord API Library for Bots](https://dpp.dev/) #### Supported Translation Engines diff --git a/rpmsrc/dtranslatebot.spec b/rpmsrc/dtranslatebot.spec index 163461d..91b971e 100644 --- a/rpmsrc/dtranslatebot.spec +++ b/rpmsrc/dtranslatebot.spec @@ -27,10 +27,14 @@ Source3: %{name}.sysusersd %if 0%{?fedora} || 0%{?rhel} BuildRequires: %{?toolset_prefix}annobin +BuildRequires: curl-devel %if 0%{?rhel} && 0%{?rhel} < 9 BuildRequires: epel-rpm-macros-systemd %endif %endif +%if 0%{?suse_version} +BuildRequires: libcurl-devel +%endif BuildRequires: cmake%{?cmake_suffix} BuildRequires: %{?toolset_prefix}gcc%{?toolset_version}-c++ BuildRequires: make diff --git a/src/core/http_request.cpp b/src/core/http_request.cpp new file mode 100644 index 0000000..0828ac2 --- /dev/null +++ b/src/core/http_request.cpp @@ -0,0 +1,94 @@ +/***************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 2026 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 "http_request.h" + +http_request::http_request() { + instance = curl_easy_init(); + if (!instance) + throw std::bad_alloc(); +} + +http_response http_request::get(const std::string &url, const dpp::http_headers &headers) { + http_response response; + curl_easy_setopt(instance, CURLOPT_URL, url.c_str()); + curl_slist *header_slist = nullptr; + if (!headers.empty()) { + for (const auto &header : headers) { + header_slist = curl_slist_append(header_slist, std::string(header.first + ": " + header.second).c_str()); + if (!header_slist) + throw std::bad_alloc(); + } + } + if (!header_slist) + curl_easy_setopt(instance, CURLOPT_HTTPHEADER, header_slist); + curl_easy_setopt(instance, CURLOPT_WRITEDATA, &response.content); + curl_easy_setopt(instance, CURLOPT_WRITEFUNCTION, &writer); + CURLcode result = curl_easy_perform(instance); + if (result == CURLE_OK) + curl_easy_getinfo(instance, CURLINFO_RESPONSE_CODE, &response.status); + curl_easy_reset(instance); + curl_slist_free_all(header_slist); + return response; +} + +http_response http_request::post(const std::string &url, const std::string &content, const std::string &content_type, const dpp::http_headers &headers) { + http_response response; + curl_easy_setopt(instance, CURLOPT_URL, url.c_str()); + curl_slist *header_slist = nullptr; + if (!content_type.empty()) { + header_slist = curl_slist_append(header_slist, std::string("Content-Type: " + content_type).c_str()); + if (!header_slist) + throw std::bad_alloc(); + } + if (!headers.empty()) { + for (const auto &header : headers) { + header_slist = curl_slist_append(header_slist, std::string(header.first + ": " + header.second).c_str()); + if (!header_slist) + throw std::bad_alloc(); + } + } + if (!header_slist) + curl_easy_setopt(instance, CURLOPT_HTTPHEADER, header_slist); + curl_easy_setopt(instance, CURLOPT_POSTFIELDS, content.data()); + curl_easy_setopt(instance, CURLOPT_POSTFIELDSIZE, content.size()); + curl_easy_setopt(instance, CURLOPT_WRITEDATA, &response.content); + curl_easy_setopt(instance, CURLOPT_WRITEFUNCTION, &writer); + CURLcode result = curl_easy_perform(instance); + if (result == CURLE_OK) + curl_easy_getinfo(instance, CURLINFO_RESPONSE_CODE, &response.status); + curl_easy_reset(instance); + curl_slist_free_all(header_slist); + return response; +} + +std::string http_request::legacy_url(const std::string &hostname, uint16_t port, const std::string &url, bool tls) { + return (tls ? "https://" : "http://") + hostname + ":" + std::to_string(port) + (url.empty() ? "/" : (url.front() != '/' ? "/" + url : url)); +} + +http_request::~http_request() { + curl_easy_cleanup(instance); +} + +size_t http_request::writer(char *source, size_t size, size_t nmemb, std::string *target) { + if (target == nullptr) + return 0; + size_t write_size = size * nmemb; + target->append(source, write_size); + return write_size; +} diff --git a/src/core/http_request.h b/src/core/http_request.h new file mode 100644 index 0000000..a1f14cf --- /dev/null +++ b/src/core/http_request.h @@ -0,0 +1,40 @@ +/***************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 2026 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 HTTP_REQUEST_H +#define HTTP_REQUEST_H +#include +#include +#include +#include "http_response.h" + +class http_request +{ +public: + http_request(); + ~http_request(); + http_response get(const std::string &url, const dpp::http_headers &headers = {}); + http_response post(const std::string &url, const std::string &content, const std::string &content_type, const dpp::http_headers &headers = {}); + static std::string legacy_url(const std::string &hostname, uint16_t port, const std::string &url, bool tls); + +private: + static size_t writer(char *source, size_t size, size_t nmemb, std::string *target); + CURL *instance; +}; + +#endif // HTTP_REQUEST_H diff --git a/src/core/http_response.h b/src/core/http_response.h new file mode 100644 index 0000000..2364e13 --- /dev/null +++ b/src/core/http_response.h @@ -0,0 +1,29 @@ +/***************************************************************************** +* dtranslatebot Discord Translate Bot +* Copyright (C) 2026 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 HTTP_RESPONSE_H +#define HTTP_RESPONSE_H +#include + +struct http_response +{ + std::string content; + long status; +}; + +#endif // HTTP_RESPONSE_H diff --git a/src/core/main.cpp b/src/core/main.cpp index 43ddd75..4bb1073 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "message_queue.h" #include "settings.h" #include "slashcommands.h" @@ -45,6 +46,12 @@ int main(int argc, char* argv[]) { if (!settings.parse_file(args.at(0))) return 1; + CURLcode result = curl_global_init(CURL_GLOBAL_DEFAULT); + if (result != CURLE_OK) { + std::cerr << "[Error] Failed to initialise curl" << std::endl; + return 1; + } + for (;;) { std::cout << "[Launch] Requesting supported languages..." << std::endl; if (!settings.get_translator()->get_languages().empty()) { @@ -88,5 +95,7 @@ int main(int argc, char* argv[]) { submit_queue.terminate(); submit_queue_loop.join(); + curl_global_cleanup(); + return 0; } diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 24181c7..b00895a 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -1,6 +1,6 @@ /***************************************************************************** * dtranslatebot Discord Translate Bot -* Copyright (C) 2023-2024 Syping +* Copyright (C) 2023-2026 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: diff --git a/src/translator/deepl/deepl.cpp b/src/translator/deepl/deepl.cpp index e81481c..6fb73c6 100644 --- a/src/translator/deepl/deepl.cpp +++ b/src/translator/deepl/deepl.cpp @@ -1,6 +1,6 @@ /***************************************************************************** * dtranslatebot Discord Translate Bot -* Copyright (C) 2024 Syping +* Copyright (C) 2024-2026 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -18,6 +18,7 @@ #include #include +#include "../../core/http_request.h" #include "deepl.h" using namespace bot::translator; using namespace std::chrono_literals; @@ -42,12 +43,13 @@ const std::vector deepl::get_languages() } try { - dpp::https_client http_request(&m_cluster, m_hostname, 443, "/v2/languages?type=target", "GET", {}, { {"Authorization"s, "DeepL-Auth-Key " + m_apiKey} }, false); - if (http_request.get_status() == 200) { - const dpp::json response = dpp::json::parse(http_request.get_content()); - if (response.is_array()) { + http_request request; + http_response response = request.get(http_request::legacy_url(m_hostname, 443, "/v2/languages?type=target", true), { {"Authorization"s, "DeepL-Auth-Key " + m_apiKey} }); + if (response.status == 200) { + const dpp::json json_response = dpp::json::parse(response.content); + if (json_response.is_array()) { m_languages.languages.clear(); - for (auto json_language = response.begin(); json_language != response.end(); json_language++) { + for (auto json_language = json_response.begin(); json_language != json_response.end(); json_language++) { if (json_language->is_object()) { language language; @@ -93,12 +95,13 @@ const std::string deepl::translate(const std::string &text, const std::string &s }; try { - dpp::https_client http_request(&m_cluster, m_hostname, 443, "/v2/translate", "POST", json_body.dump(), http_headers, false); - if (http_request.get_status() == 200) { - const dpp::json response = dpp::json::parse(http_request.get_content()); - if (response.is_object()) { - auto translations = response.find("translations"); - if (translations != response.end() && translations->is_array()) { + http_request request; + http_response response = request.post(http_request::legacy_url(m_hostname, 443, "/v2/translate", true), json_body.dump(), "application/json", { {"Authorization"s, "DeepL-Auth-Key " + m_apiKey} }); + if (response.status == 200) { + const dpp::json json_response = dpp::json::parse(response.content); + if (json_response.is_object()) { + auto translations = json_response.find("translations"); + if (translations != json_response.end() && translations->is_array()) { for (auto translation = translations->begin(); translation != translations->end(); translation++) { auto tr_text = translation->find("text"); if (tr_text != translation->end()) diff --git a/src/translator/deepl/deepl.h b/src/translator/deepl/deepl.h index b4e0d0d..6fd90f9 100644 --- a/src/translator/deepl/deepl.h +++ b/src/translator/deepl/deepl.h @@ -1,6 +1,6 @@ /***************************************************************************** * dtranslatebot Discord Translate Bot -* Copyright (C) 2024 Syping +* Copyright (C) 2024-2026 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -32,7 +32,6 @@ namespace bot { const std::string translate(const std::string &text, const std::string &source, const std::string &target) override; private: - dpp::cluster m_cluster; std::string m_apiKey; std::string m_hostname; supported_languages m_languages; diff --git a/src/translator/libretranslate/libretranslate.cpp b/src/translator/libretranslate/libretranslate.cpp index 3d82a89..96d6f36 100644 --- a/src/translator/libretranslate/libretranslate.cpp +++ b/src/translator/libretranslate/libretranslate.cpp @@ -1,6 +1,6 @@ /***************************************************************************** * dtranslatebot Discord Translate Bot -* Copyright (C) 2023-2024 Syping +* Copyright (C) 2023-2026 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -18,6 +18,7 @@ #include #include +#include "../../core/http_request.h" #include "libretranslate.h" using namespace bot::translator; using namespace std::chrono_literals; @@ -42,12 +43,13 @@ const std::vector libretranslate::get_languages() } try { - dpp::https_client http_request(&m_cluster, 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()) { + http_request request; + http_response response = request.get(http_request::legacy_url(m_hostname, m_port, m_url + "languages", m_tls)); + if (response.status == 200) { + const dpp::json json_response = dpp::json::parse(response.content); + if (json_response.is_array()) { m_languages.languages.clear(); - for (auto json_language = response.begin(); json_language != response.end(); json_language++) { + for (auto json_language = json_response.begin(); json_language != json_response.end(); json_language++) { if (json_language->is_object()) { language language; @@ -91,12 +93,13 @@ const std::string libretranslate::translate(const std::string &text, const std:: json_body["apiKey"] = m_apiKey; try { - dpp::https_client http_request(&m_cluster, 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()) + http_request request; + http_response response = request.post(http_request::legacy_url(m_hostname, m_port, m_url + "translate", m_tls), json_body.dump(), "application/json"); + if (response.status == 200) { + const dpp::json json_response = dpp::json::parse(response.content); + if (json_response.is_object()) { + auto tr_text = json_response.find("translatedText"); + if (tr_text != json_response.end()) return *tr_text; } } diff --git a/src/translator/libretranslate/libretranslate.h b/src/translator/libretranslate/libretranslate.h index 75306a8..04c0439 100644 --- a/src/translator/libretranslate/libretranslate.h +++ b/src/translator/libretranslate/libretranslate.h @@ -34,7 +34,6 @@ namespace bot { const std::string translate(const std::string &text, const std::string &source, const std::string &target) override; private: - dpp::cluster m_cluster; std::string m_apiKey; std::string m_hostname; supported_languages m_languages; diff --git a/src/translator/lingvatranslate/lingvatranslate.cpp b/src/translator/lingvatranslate/lingvatranslate.cpp index b8352e5..2231398 100644 --- a/src/translator/lingvatranslate/lingvatranslate.cpp +++ b/src/translator/lingvatranslate/lingvatranslate.cpp @@ -1,6 +1,6 @@ /***************************************************************************** * dtranslatebot Discord Translate Bot -* Copyright (C) 2024 Syping +* Copyright (C) 2024-2026 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -19,6 +19,7 @@ #include #include #include +#include "../../core/http_request.h" #include "lingvatranslate.h" using namespace bot::translator; using namespace std::chrono_literals; @@ -43,12 +44,13 @@ const std::vector lingvatranslate::get_languages() } try { - dpp::https_client http_request(&m_cluster, m_hostname, m_port, m_url + "api/v1/languages/target", "GET", {}, {}, !m_tls); - if (http_request.get_status() == 200) { - const dpp::json response = dpp::json::parse(http_request.get_content()); - if (response.is_object()) { - auto languages = response.find("languages"); - if (languages != response.end()) { + http_request request; + http_response response = request.get(http_request::legacy_url(m_hostname, m_port, m_url + "api/v1/languages/target", m_tls)); + if (response.status == 200) { + const dpp::json json_response = dpp::json::parse(response.content); + if (json_response.is_object()) { + auto languages = json_response.find("languages"); + if (languages != json_response.end()) { m_languages.languages.clear(); for (auto json_language = languages->begin(); json_language != languages->end(); json_language++) { if (json_language->is_object()) { @@ -81,14 +83,15 @@ const std::vector lingvatranslate::get_languages() const std::string lingvatranslate::translate(const std::string &text, const std::string &source, const std::string &target) { try { - dpp::https_client http_request(&m_cluster, m_hostname, m_port, m_url + "api/v1/" + source + "/" + target + "/" + dpp::utility::url_encode(text), "GET", {}, {}, !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("translation"); - if (tr_text != response.end()) + http_request request; + http_response response = request.get(http_request::legacy_url(m_hostname, m_port, m_url + "api/v1/" + source + "/" + target + "/" + dpp::utility::url_encode(text), m_tls)); + if (response.status == 200) { + const dpp::json json_response = dpp::json::parse(response.content); + if (json_response.is_object()) { + auto tr_text = json_response.find("translation"); + if (tr_text != json_response.end()) return *tr_text; - } + } } } catch (const std::exception &exception) { diff --git a/src/translator/lingvatranslate/lingvatranslate.h b/src/translator/lingvatranslate/lingvatranslate.h index 805dd9b..cf65739 100644 --- a/src/translator/lingvatranslate/lingvatranslate.h +++ b/src/translator/lingvatranslate/lingvatranslate.h @@ -34,7 +34,6 @@ namespace bot { const std::string translate(const std::string &text, const std::string &source, const std::string &target) override; private: - dpp::cluster m_cluster; std::string m_hostname; supported_languages m_languages; uint16_t m_port; diff --git a/src/translator/mozhi/mozhi.cpp b/src/translator/mozhi/mozhi.cpp index e5e8ea1..f9ffda7 100644 --- a/src/translator/mozhi/mozhi.cpp +++ b/src/translator/mozhi/mozhi.cpp @@ -1,6 +1,6 @@ /***************************************************************************** * dtranslatebot Discord Translate Bot -* Copyright (C) 2024 Syping +* Copyright (C) 2024-2026 Syping * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -19,6 +19,7 @@ #include #include #include +#include "../../core/http_request.h" #include "mozhi.h" using namespace bot::translator; using namespace std::chrono_literals; @@ -46,12 +47,13 @@ const std::vector mozhi::get_languages() const std::string parameters = dpp::utility::make_url_parameters({ {"engine"s, m_engine} }); - dpp::https_client http_request(&m_cluster, m_hostname, m_port, m_url + "api/target_languages" + parameters, "GET", {}, {}, !m_tls); - if (http_request.get_status() == 200) { - const dpp::json response = dpp::json::parse(http_request.get_content()); - if (response.is_array()) { + http_request request; + http_response response = request.get(http_request::legacy_url(m_hostname, m_port, m_url + "api/target_languages", m_tls)); + if (response.status == 200) { + const dpp::json json_response = dpp::json::parse(response.content); + if (json_response.is_array()) { m_languages.languages.clear(); - for (auto json_language = response.begin(); json_language != response.end(); json_language++) { + for (auto json_language = json_response.begin(); json_language != json_response.end(); json_language++) { if (json_language->is_object()) { language language; @@ -87,12 +89,13 @@ const std::string mozhi::translate(const std::string &text, const std::string &s {"to"s, target}, {"text"s, text}, }); - dpp::https_client http_request(&m_cluster, m_hostname, m_port, m_url + "api/translate" + parameters, "GET", {}, {}, !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("translated-text"); - if (tr_text != response.end()) + http_request request; + http_response response = request.get(http_request::legacy_url(m_hostname, m_port, m_url + "api/translate" + parameters, m_tls)); + if (response.status == 200) { + const dpp::json json_response = dpp::json::parse(response.content); + if (json_response.is_object()) { + auto tr_text = json_response.find("translated-text"); + if (tr_text != json_response.end()) return *tr_text; } } diff --git a/src/translator/mozhi/mozhi.h b/src/translator/mozhi/mozhi.h index 8a8735a..1602096 100644 --- a/src/translator/mozhi/mozhi.h +++ b/src/translator/mozhi/mozhi.h @@ -34,7 +34,6 @@ namespace bot { const std::string translate(const std::string &text, const std::string &source, const std::string &target) override; private: - dpp::cluster m_cluster; std::string m_engine; std::string m_hostname; supported_languages m_languages;