diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9a45441 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +* text=auto eol=lf + +# Configuration files +*.json text eol=crlf + +# Development files +CMakeLists.txt text eol=lf +*.cmake text eol=lf +*.cpp text eol=lf +*.h text eol=lf + +# BSD and Linux development files +*.pc.in text eol=lf + +# Windows development files +*.rc.in text encoding=cp1252 eol=crlf diff --git a/.github/workflows/linux-rpm.yml b/.github/workflows/linux-rpm.yml new file mode 100644 index 0000000..b51f55d --- /dev/null +++ b/.github/workflows/linux-rpm.yml @@ -0,0 +1,45 @@ +name: Linux +on: push +jobs: + Release: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: Enterprise Linux 7 + version: el7 + - name: Enterprise Linux 8 + version: el8 + - name: Enterprise Linux 9 + version: el9 + - name: openSUSE Leap 15.5 + version: lp155 + steps: + - name: Cloning + uses: actions/checkout@v4 + - name: Preparing + run: mkdir -m 777 ${{github.workspace}}/rpms + - name: Build RPM + uses: addnab/docker-run-action@v3 + with: + image: docker.io/syping/dtranslatebot-build:${{matrix.version}} + options: -v ${{github.workspace}}:/home/rpmbuild/dtranslatebot -v ${{github.workspace}}/rpms:/home/rpmbuild/rpmbuild/RPMS + run: | + VERSION=$(cat dtranslatebot/CMakeLists.txt | grep -oP "project\(dtranslatebot VERSION \K(\S*)(?= LANGUAGES CXX\))") + mkdir -p dtranslatebot-$VERSION + shopt -s extglob + cp -R dtranslatebot/!(rpms|rpmsrc) \ + dtranslatebot-$VERSION + tar cfz dtranslatebot-$VERSION.tar.gz dtranslatebot-$VERSION + cp dtranslatebot-$VERSION.tar.gz \ + dtranslatebot/rpmsrc/!(*.spec) \ + rpmbuild/SOURCES + cp dtranslatebot/rpmsrc/*.spec \ + rpmbuild/SPECS + rpmbuild -ba rpmbuild/SPECS/dtranslatebot.spec + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{matrix.name}} + path: | + ${{github.workspace}}/rpms/x86_64/*.rpm diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..c0607fc --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,43 @@ +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 59e34f7..29bf456 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,34 +17,43 @@ ****************************************************************************]] cmake_minimum_required(VERSION 3.16) -project(dtranslatebot VERSION 0.1 LANGUAGES CXX) +cmake_policy(VERSION 3.16...3.27) +project(dtranslatebot VERSION 0.2.0 LANGUAGES CXX) include(GNUInstallDirs) # dtranslatebot Source files set(DTRANSLATEBOT_HEADERS - src/database_core.h - src/database_file.h - src/message_queue.h - src/regex.h - src/settings.h - src/settings_types.h - src/slashcommands.h - src/submit_queue.h - src/translator_core.h - src/translator_libretranslate.h - src/webhook_push.h + src/core/database.h + src/core/message_queue.h + src/core/regex.h + src/core/settings.h + src/core/settings_types.h + src/core/slashcommands.h + src/core/submit_queue.h + src/core/translator.h + src/core/webhook_push.h + src/database/file/file.h + src/translator/deepl/deepl.h + src/translator/mozhi/mozhi.h + src/translator/libretranslate/libretranslate.h + src/translator/lingvatranslate/lingvatranslate.h + src/translator/stub/stub.h ) set(DTRANSLATEBOT_SOURCES - src/database_core.cpp - src/database_file.cpp - src/main.cpp - src/message_queue.cpp - src/settings.cpp - src/slashcommands.cpp - src/submit_queue.cpp - src/translator_core.cpp - src/translator_libretranslate.cpp - src/webhook_push.cpp + src/core/database.cpp + src/core/main.cpp + src/core/message_queue.cpp + src/core/settings.cpp + src/core/slashcommands.cpp + src/core/submit_queue.cpp + src/core/translator.cpp + src/core/webhook_push.cpp + src/database/file/file.cpp + src/translator/deepl/deepl.cpp + src/translator/mozhi/mozhi.cpp + src/translator/libretranslate/libretranslate.cpp + src/translator/lingvatranslate/lingvatranslate.cpp + src/translator/stub/stub.cpp ) # dtranslatebot Module Path @@ -63,21 +72,59 @@ if (WITH_BOOST) endif() # D++ Discord API Library for Bots -find_package(DPP REQUIRED) +option(WITH_DPP_STATIC_BUNDLE "Build with DPP Static Bundle" OFF) +if (WITH_DPP_STATIC_BUNDLE) + include(DPPStaticBundle) +else() + find_package(DPP REQUIRED) +endif() # pthread Support set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) +# dtranslatebot Win32 Shared Resources +if (WIN32) + configure_file(src/resources/win32/dtranslatebot.rc.in "${dtranslatebot_BINARY_DIR}/resources/win32/dtranslatebot.rc" @ONLY) + list(APPEND DTRANSLATEBOT_RESOURCES + "${dtranslatebot_BINARY_DIR}/resources/win32/dtranslatebot.rc" + ) +endif() + +# dtranslatebot systemd Service +if (UNIX AND NOT APPLE) + option(WITH_SYSTEMD "Build with systemd Support" OFF) + if (WITH_SYSTEMD) + find_program(SYSTEMD_ESCAPE_EXECUTABLE NAMES systemd-escape) + if (DEFINED SYSTEMD_ESCAPE_EXECUTABLE) + execute_process( + COMMAND "${SYSTEMD_ESCAPE_EXECUTABLE}" "${CMAKE_INSTALL_FULL_LOCALSTATEDIR}/lib/dtranslatebot" + OUTPUT_VARIABLE dtranslatebot_SERVICE_WORKDIR + ) + string(STRIP "${dtranslatebot_SERVICE_WORKDIR}" dtranslatebot_SERVICE_WORKDIR) + else() + set(dtranslatebot_SERVICE_WORKDIR "${CMAKE_INSTALL_FULL_LOCALSTATEDIR}/lib/dtranslatebot") + endif() + configure_file(src/systemd/dtranslatebot.service.in "${dtranslatebot_BINARY_DIR}/systemd/service/dtranslatebot.service" @ONLY) + configure_file(src/systemd/dtranslatebot.sysusersd.in "${dtranslatebot_BINARY_DIR}/systemd/sysusers.d/dtranslatebot.conf" @ONLY) + install(FILES "${dtranslatebot_BINARY_DIR}/systemd/service/dtranslatebot.service" DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/systemd/system") + install(FILES "${dtranslatebot_BINARY_DIR}/systemd/sysusers.d/dtranslatebot.conf" DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/sysusers.d") + endif() +endif() + # dtranslatebot Target + Installs -add_executable(dtranslatebot ${DTRANSLATEBOT_HEADERS} ${DTRANSLATEBOT_SOURCES}) +add_executable(dtranslatebot ${DTRANSLATEBOT_HEADERS} ${DTRANSLATEBOT_SOURCES} ${DTRANSLATEBOT_RESOURCES}) +if (WITH_DPP_STATIC_BUNDLE) + add_dependencies(dtranslatebot DPP) +endif() target_compile_definitions(dtranslatebot PRIVATE + ${DPP_DEFINITIONS} $<$:DTRANSLATEBOT_USE_BOOST_REGEX> ) if (MSVC AND MSVC_VERSION GREATER_EQUAL 1914) target_compile_options(dtranslatebot PRIVATE $<$:/Zc:__cplusplus>) endif() -target_link_libraries(dtranslatebot PRIVATE Threads::Threads ${DPP_LIBRARIES} ${DTRANSLATEBOT_LIBRARIES}) +target_link_libraries(dtranslatebot PRIVATE ${DTRANSLATEBOT_LIBRARIES} ${DPP_LIBRARIES} Threads::Threads) target_include_directories(dtranslatebot PRIVATE ${DPP_INCLUDE_DIR}) set_target_properties(dtranslatebot PROPERTIES CXX_STANDARD 17 diff --git a/README.md b/README.md index de6ccdc..0e3049c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Open Source Discord Translation Bot - Translate incoming channel messages to a Webhook -- Support configuration through slashcommands and JSON +- Support configuration through slash commands and JSON - Cross-Platform #### Build Dependencies @@ -10,8 +10,11 @@ Open Source Discord Translation Bot - Compiler with C++17 Support - [D++: A C++ Discord API Library for Bots](https://dpp.dev/) -#### Runtime Dependencies -- [LibreTranslate](https://libretranslate.com/) +#### Supported Translation Engines +- [LibreTranslate](https://libretranslate.com/) (Default) +- [Lingva Translate](https://lingva.ml/) +- [Mozhi](https://codeberg.org/aryak/mozhi) +- [DeepL](https://deepl.com/) (Experimental) #### Build dtranslatebot @@ -23,4 +26,6 @@ sudo cmake --install dtranslatebot-build ``` ##### Optional CMake flags -`-DWITH_BOOST=TRUE` +`-DWITH_BOOST=TRUE` +`-DWITH_DPP_STATIC_BUNDLE=TRUE` +`-DWITH_SYSTEMD=TRUE` diff --git a/cmake/ArgumentPassthrough.cmake b/cmake/ArgumentPassthrough.cmake new file mode 100644 index 0000000..a0367d2 --- /dev/null +++ b/cmake/ArgumentPassthrough.cmake @@ -0,0 +1,65 @@ +#[[************************************************************************** +* 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. +****************************************************************************]] + +if (DEFINED CMAKE_BUILD_TYPE) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}") +endif() +if (DEFINED CMAKE_C_COMPILER) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}") + list(APPEND CMAKE_PASSTHROUGH_ENV "CC=${CMAKE_C_COMPILER}") +elseif ("$ENV{MSYSTEM}" STREQUAL "CLANG64") + list(APPEND CMAKE_PASSTHROUGH_ENV "CC=clang") +endif() +if (DEFINED CMAKE_C_COMPILER_TARGET) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_C_COMPILER_TARGET=${CMAKE_C_COMPILER_TARGET}") +endif() +if (DEFINED CMAKE_CXX_COMPILER) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}") + list(APPEND CMAKE_PASSTHROUGH_ENV "CXX=${CMAKE_CXX_COMPILER}") +elseif ("$ENV{MSYSTEM}" STREQUAL "CLANG64") + list(APPEND CMAKE_PASSTHROUGH_ENV "CXX=clang++") +endif() +if (DEFINED CMAKE_CXX_COMPILER_TARGET) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_CXX_COMPILER_TARGET=${CMAKE_CXX_COMPILER_TARGET}") +endif() +if (DEFINED CMAKE_RC_COMPILER) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_RC_COMPILER=${CMAKE_RC_COMPILER}") +endif() +if (DEFINED CMAKE_STRIP) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_STRIP=${CMAKE_STRIP}") +endif() +if (DEFINED CMAKE_SYSROOT) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_SYSROOT=${CMAKE_SYSROOT}") +endif() +if (DEFINED CMAKE_SYSTEM_NAME AND NOT CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}") +endif() +if (DEFINED CMAKE_SYSTEM_PROCESSOR AND NOT CMAKE_SYSTEM_PROCESSOR STREQUAL CMAKE_HOST_SYSTEM_PROCESSOR) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_SYSTEM_PROCESSOR=${CMAKE_SYSTEM_PROCESSOR}") +endif() +if (DEFINED CMAKE_SYSTEM_VERSION AND NOT CMAKE_SYSTEM_VERSION STREQUAL CMAKE_HOST_SYSTEM_VERSION) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_SYSTEM_VERSION=${CMAKE_SYSTEM_VERSION}") +endif() +if (DEFINED CMAKE_TOOLCHAIN_FILE) + list(APPEND CMAKE_PASSTHROUGH_ARGS "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}") +endif() +if (DEFINED CMAKE_PASSTHROUGH_ENV) + set(CMAKE_PASSTHROUGH_ENV + "${CMAKE_COMMAND}" -E env ${CMAKE_PASSTHROUGH_ENV} + ) +endif() diff --git a/cmake/DPPStaticBundle.cmake b/cmake/DPPStaticBundle.cmake new file mode 100644 index 0000000..3c214ed --- /dev/null +++ b/cmake/DPPStaticBundle.cmake @@ -0,0 +1,119 @@ +#[[************************************************************************** +* 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(ArgumentPassthrough) + +# OpenSSL needs to be configured with perl and build with make +find_program(MAKE_EXECUTABLE NAMES make gmake) +if (NOT DEFINED MAKE_EXECUTABLE) + message(SEND_ERROR "make not found") +endif() + +find_program(NPROC_EXECUTABLE nproc) +if (DEFINED NPROC_EXECUTABLE) + execute_process( + COMMAND "${NPROC_EXECUTABLE}" + OUTPUT_VARIABLE NPROC + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + set(MAKE_JOBS_ARG "-j${NPROC}") +endif() + +find_program(PERL_EXECUTABLE NAMES perl) +if (NOT DEFINED PERL_EXECUTABLE) + message(SEND_ERROR "perl not found") +endif() + +include(ExternalProject) +ExternalProject_Add(ZLIB + URL https://www.zlib.net/zlib-1.3.1.tar.xz + URL_HASH SHA256=38ef96b8dfe510d42707d9c781877914792541133e1870841463bfa73f883e32 + CMAKE_ARGS + -DBUILD_SHARED_LIBS=OFF + ${CMAKE_PASSTHROUGH_ARGS} + "-DCMAKE_INSTALL_PREFIX=" + -DZLIB_BUILD_EXAMPLES=OFF + ${ZLIB_CONFIGURE_ARGS} +) +ExternalProject_Get_Property(ZLIB INSTALL_DIR) +set(ZLIB_INSTALL_DIR "${INSTALL_DIR}") + +set(OPENSSL_PLATFORM_ARG $<$:mingw64>) +ExternalProject_Add(OpenSSL + URL https://github.com/openssl/openssl/releases/download/openssl-3.0.17/openssl-3.0.17.tar.gz + URL_HASH SHA256=dfdd77e4ea1b57ff3a6dbde6b0bdc3f31db5ac99e7fdd4eaf9e1fbb6ec2db8ce + CONFIGURE_COMMAND + ${CMAKE_PASSTHROUGH_ENV} + "${PERL_EXECUTABLE}" + "/Configure" + "--prefix=" + $<$:-d> + no-dso + no-dtls + no-engine + no-shared + no-zlib + ${OPENSSL_PLATFORM_ARG} + ${OPENSSL_CONFIGURE_ARGS} + BUILD_COMMAND "${MAKE_EXECUTABLE}" ${MAKE_JOBS_ARG} build_libs + INSTALL_COMMAND "${MAKE_EXECUTABLE}" ${MAKE_JOBS_ARG} install_dev +) +ExternalProject_Get_Property(OpenSSL INSTALL_DIR) +set(OpenSSL_INSTALL_DIR "${INSTALL_DIR}") + +ExternalProject_Add(DPP + URL https://github.com/brainboxdotcc/DPP/archive/refs/tags/v10.1.3.tar.gz + URL_HASH SHA256=a32d94dcd6b23430afff82918234e4e28e0616bd2ddf743c5ab2f1778c5a600b + CMAKE_ARGS + -DAVX_TYPE=AVX0 + -DBUILD_SHARED_LIBS=OFF + -DBUILD_VOICE_SUPPORT=OFF + ${CMAKE_PASSTHROUGH_ARGS} + "-DCMAKE_INSTALL_PREFIX=" + -DDPP_BUILD_TEST=OFF + -DDPP_NO_CORO=ON + -DDPP_NO_VCPKG=ON + -DRUN_LDCONFIG=OFF + "-DOpenSSL_ROOT=${OpenSSL_INSTALL_DIR}" + "-DZLIB_ROOT=${ZLIB_INSTALL_DIR}" + ${DPP_CONFIGURE_ARGS} + DEPENDS OpenSSL ZLIB +) +ExternalProject_Get_Property(DPP INSTALL_DIR) +set(DPP_INSTALL_DIR "${INSTALL_DIR}") +set(DPP_INCLUDE_DIR "${DPP_INSTALL_DIR}/include") +set(DPP_LIBRARIES + -Wl,-Bstatic + "-L${DPP_INSTALL_DIR}/lib" + "-L${DPP_INSTALL_DIR}/lib64" + dpp + "-L${OpenSSL_INSTALL_DIR}/lib" + "-L${OpenSSL_INSTALL_DIR}/lib64" + ssl + crypto + "-L${ZLIB_INSTALL_DIR}/lib" + "-L${ZLIB_INSTALL_DIR}/lib64" + $,zlibstatic,z> + -Wl,-Bdynamic +) +if (WIN32) + set(DPP_DEFINITIONS DPP_STATIC) + list(APPEND DPP_LIBRARIES + ws2_32 + ) +endif() diff --git a/cmake/FindDPP.cmake b/cmake/FindDPP.cmake index a555a7f..55c928e 100644 --- a/cmake/FindDPP.cmake +++ b/cmake/FindDPP.cmake @@ -1,4 +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}) +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.example.json b/etc/dtranslatebot.example.json deleted file mode 100644 index 202a0ee..0000000 --- a/etc/dtranslatebot.example.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "guilds": { - "$guild1_id": { - "$channel1_id": { - "source": "en", - "target": "de", - "webhook": "https://discord.com/api/webhooks/$guild1_de_webhook_id/$guild1_de_webhook_token" - }, - "$channel2_id": { - "source": "de", - "target": "en", - "webhook": "https://discord.com/api/webhooks/$guild1_en_webhook_id/$guild1_en_webhook_token" - } - }, - "My Discord Guild": { - "id": $guild2_id, - "General English": { - "id": $channel3_id, - "source": "en", - "target": { - "de": "https://discord.com/api/webhooks/$guild2_de_webhook_id/$guild2_de_webhook_token", - "fr": "https://discord.com/api/webhooks/$guild2_fr_webhook_id/$guild2_fr_webhook_token" - } - } - } - }, - "preferred_lang": ["en", "de", "fr", ...], - "storage": "$working_directory", - "user": { - "avatar_size": 256 - }, - "token": "$bot_token", - "translator": { - "url": "http://127.0.0.1:5000/", - "apiKey": "" - } -} diff --git a/etc/dtranslatebot.json b/etc/dtranslatebot.json deleted file mode 100644 index 28381db..0000000 --- a/etc/dtranslatebot.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "token": "", - "translator": { - "url": "http://127.0.0.1:5000/", - "apiKey": "" - } -} diff --git a/rpmsrc/dtranslatebot.json b/rpmsrc/dtranslatebot.json new file mode 100644 index 0000000..e0a0dc0 --- /dev/null +++ b/rpmsrc/dtranslatebot.json @@ -0,0 +1,14 @@ +{ + /* + A DISCORD BOT TOKEN IS NECESSARY TO USE THIS APPLICATION! + + You can get a Bot Token from the Discord Developer Portal + by creating a New Application: + https://discord.com/developers/applications + */ + + "token": "", + "translator": { + "type": "stub" + } +} diff --git a/rpmsrc/dtranslatebot.service b/rpmsrc/dtranslatebot.service new file mode 100644 index 0000000..7bd129f --- /dev/null +++ b/rpmsrc/dtranslatebot.service @@ -0,0 +1,13 @@ +[Unit] +Description=Discord Translation Bot +After=network.target + +[Service] +User=dtranslatebot +Group=dtranslatebot +WorkingDirectory=/var/lib/dtranslatebot +ExecStart=/usr/bin/dtranslatebot /etc/dtranslatebot.json +Restart=on-abnormal + +[Install] +WantedBy=multi-user.target diff --git a/rpmsrc/dtranslatebot.spec b/rpmsrc/dtranslatebot.spec new file mode 100644 index 0000000..163461d --- /dev/null +++ b/rpmsrc/dtranslatebot.spec @@ -0,0 +1,96 @@ +%global _lto_cflags %{?_lto_cflags} -ffat-lto-objects + +%if 0%{?rhel} && 0%{?rhel} < 8 +%global cmake %{?cmake3} +%global cmake_build %{?cmake3_build} +%global cmake_install %{?cmake3_install} +%global cmake_suffix 3 +%global toolset_prefix devtoolset-9- +%endif +%if 0%{?rhel} && 0%{?rhel} == 8 +%global toolset_prefix gcc-toolset-9- +%endif +%if 0%{?suse_version} && 0%{?suse_version} < 1600 +%global toolset_version 9 +%endif + +Name: dtranslatebot +Version: 0.2.0 +Release: 1%{?dist} +Summary: Discord Translation Bot +License: BSD-2-Clause +URL: https://github.com/Syping/%{name} +Source0: %{name}-%{version}.tar.gz +Source1: %{name}.json +Source2: %{name}.service +Source3: %{name}.sysusersd + +%if 0%{?fedora} || 0%{?rhel} +BuildRequires: %{?toolset_prefix}annobin +%if 0%{?rhel} && 0%{?rhel} < 9 +BuildRequires: epel-rpm-macros-systemd +%endif +%endif +BuildRequires: cmake%{?cmake_suffix} +BuildRequires: %{?toolset_prefix}gcc%{?toolset_version}-c++ +BuildRequires: make +BuildRequires: perl(IPC::Cmd) +BuildRequires: systemd-rpm-macros +%systemd_requires +%if 0%{?fedora} || 0%{?rhel} +%sysusers_requires_compat +%endif + +%description +dtranslatebot is a Discord Bot which translate incoming Discord messages to Discord webhooks. + +%prep +%setup -q + +%build +%if 0%{?rhel} && 0%{?rhel} < 8 +source /opt/rh/devtoolset-9/enable +%endif +%if 0%{?rhel} && 0%{?rhel} == 8 +source /opt/rh/gcc-toolset-9/enable +%endif +%cmake \ + -DCMAKE_BUILD_TYPE=Release \ +%if 0%{?toolset_version} + -DCMAKE_C_COMPILER=gcc-%{toolset_version} \ + -DCMAKE_CXX_COMPILER=g++-%{toolset_version} \ +%endif + -DWITH_DPP_STATIC_BUNDLE=TRUE \ + -DWITH_SYSTEMD=TRUE +%cmake_build + +%install +%cmake_install +mkdir -p %{buildroot}%{_localstatedir}/lib/%{name} +install -p -D -m 0644 %{SOURCE1} %{buildroot}%{_sysconfdir}/%{name}.json +install -p -D -m 0644 %{SOURCE2} %{buildroot}%{_unitdir}/%{name}.service +install -p -D -m 0644 %{SOURCE3} %{buildroot}%{_sysusersdir}/%{name}.conf + +%pre +%if 0%{?fedora} || 0%{?rhel} +%sysusers_create_compat %{SOURCE3} +%endif +%if 0%{?suse_version} +%sysusers_create_package %{name} %{SOURCE3} +%endif + +%post +%systemd_post %{name}.service + +%preun +%systemd_preun %{name}.service + +%postun +%systemd_postun_with_restart %{name}.service + +%files +%{_bindir}/%{name} +%{_unitdir}/%{name}.service +%{_sysusersdir}/%{name}.conf +%config(noreplace) %attr(0640,root,%{name}) %{_sysconfdir}/%{name}.json +%dir %attr(0750,%{name},%{name}) %{_localstatedir}/lib/%{name} diff --git a/rpmsrc/dtranslatebot.sysusersd b/rpmsrc/dtranslatebot.sysusersd new file mode 100644 index 0000000..f181398 --- /dev/null +++ b/rpmsrc/dtranslatebot.sysusersd @@ -0,0 +1,2 @@ +#Type Name ID GECOS Home directory Shell +u dtranslatebot - "Discord Translation Bot" /var/lib/translatebot - diff --git a/src/database_core.cpp b/src/core/database.cpp similarity index 97% rename from src/database_core.cpp rename to src/core/database.cpp index b540f36..2911138 100644 --- a/src/database_core.cpp +++ b/src/core/database.cpp @@ -19,17 +19,9 @@ #ifndef NDEBUG #include #endif -#include "database_core.h" +#include "database.h" using namespace bot::database; -database::database() -{ -} - -database::~database() -{ -} - void database::add_channel_target(dpp::snowflake guild_id, dpp::snowflake channel_id, const bot::settings::target &target) { #ifndef NDEBUG diff --git a/src/database_core.h b/src/core/database.h similarity index 90% rename from src/database_core.h rename to src/core/database.h index 6526eda..2589f46 100644 --- a/src/database_core.h +++ b/src/core/database.h @@ -16,8 +16,8 @@ * responsible for anything with use of the software, you are self responsible. *****************************************************************************/ -#ifndef DATABASE_CORE_H -#define DATABASE_CORE_H +#ifndef DATABASE_H +#define DATABASE_H #include "settings_types.h" @@ -30,8 +30,10 @@ namespace bot { class database { public: - explicit database(); - virtual ~database(); + explicit database() = default; + database(const database&) = delete; + database& operator=(const database&) = delete; + virtual ~database() = default; virtual void add_channel_target(dpp::snowflake guild_id, dpp::snowflake channel_id, const bot::settings::target &target); virtual void delete_channel(dpp::snowflake guild_id, dpp::snowflake channel_id); virtual void delete_channel_target(dpp::snowflake guild_id, dpp::snowflake channel_id, const std::string &target); @@ -52,4 +54,4 @@ namespace bot { } } -#endif // DATABASE_CORE_H +#endif // DATABASE_H diff --git a/src/main.cpp b/src/core/main.cpp similarity index 70% rename from src/main.cpp rename to src/core/main.cpp index 87bd82e..43ddd75 100644 --- a/src/main.cpp +++ b/src/core/main.cpp @@ -24,22 +24,39 @@ #include "message_queue.h" #include "settings.h" #include "slashcommands.h" +using namespace std::chrono_literals; int main(int argc, char* argv[]) { - if (argc != 2) { - std::cout << "Usage: " << argv[0] << " [json]" << std::endl; + bool flag_wait_for_translator = false; + std::vector args; + for (size_t i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--wait-for-translator")) + flag_wait_for_translator = true; + else + args.push_back(argv[i]); + } + if (args.size() != 1) { + std::cout << "Usage: " << argv[0] << " [--wait-for-translator] [json]" << std::endl; return 0; } std::cout << "[Launch] Processing configuration..." << std::endl; bot::settings::settings settings; - if (!settings.parse_file(argv[1])) + if (!settings.parse_file(args.at(0))) return 1; - std::cout << "[Launch] Requesting supported languages..." << std::endl; - if (settings.get_translator()->get_languages().empty()) { - std::cerr << "[Error] Failed to initialise translateable languages" << std::endl; - return 2; + for (;;) { + std::cout << "[Launch] Requesting supported languages..." << std::endl; + if (!settings.get_translator()->get_languages().empty()) { + break; + } + else if (flag_wait_for_translator) { + std::this_thread::sleep_for(5000ms); + } + else { + std::cerr << "[Error] Failed to initialise translateable languages" << std::endl; + return 1; + } } dpp::cluster bot(settings.token(), dpp::i_default_intents | dpp::i_message_content); @@ -55,7 +72,7 @@ int main(int argc, char* argv[]) { bot.on_message_create(std::bind(&bot::message_queue::process_message_event, &message_queue, &bot, &settings, std::placeholders::_1)); bot.on_slashcommand(std::bind(&bot::slashcommands::process_command_event, &bot, &settings, std::placeholders::_1)); - bot.on_ready([&bot, &settings](const dpp::ready_t &event) { + bot.on_ready([&bot, &settings]([[maybe_unused]] const dpp::ready_t &event) { if (dpp::run_once()) { bot::slashcommands::register_commands(&bot, &settings); } diff --git a/src/message_queue.cpp b/src/core/message_queue.cpp similarity index 94% rename from src/message_queue.cpp rename to src/core/message_queue.cpp index 7276de3..a4f1085 100644 --- a/src/message_queue.cpp +++ b/src/core/message_queue.cpp @@ -19,7 +19,7 @@ #include #include "message_queue.h" #include "settings.h" -using namespace bot; +using bot::message_queue; using namespace std::chrono_literals; void message_queue::add(const message &message) @@ -36,6 +36,10 @@ void message_queue::add(message &&message) void message_queue::process_message_event(dpp::cluster *bot, bot::settings::settings *settings, const dpp::message_create_t &event) { + // We check for conditions we want to skip translation for + if (event.msg.author.id == bot->me.id || event.msg.content.empty() || event.msg.has_thread()) + return; + if (event.msg.webhook_id) { const std::lock_guard guard(*settings); @@ -44,10 +48,6 @@ void message_queue::process_message_event(dpp::cluster *bot, bot::settings::sett 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); if (const bot::settings::channel *channel = settings->get_channel(event.msg.guild_id, event.msg.channel_id)) { bot::message message; diff --git a/src/message_queue.h b/src/core/message_queue.h similarity index 100% rename from src/message_queue.h rename to src/core/message_queue.h diff --git a/src/regex.h b/src/core/regex.h similarity index 100% rename from src/regex.h rename to src/core/regex.h diff --git a/src/settings.cpp b/src/core/settings.cpp similarity index 68% rename from src/settings.cpp rename to src/core/settings.cpp index cbce34a..24181c7 100644 --- a/src/settings.cpp +++ b/src/core/settings.cpp @@ -19,19 +19,23 @@ #include #include #include -#include "database_file.h" #include "settings.h" -#include "translator_libretranslate.h" +#include "../database/file/file.h" +#include "../translator/deepl/deepl.h" +#include "../translator/mozhi/mozhi.h" +#include "../translator/libretranslate/libretranslate.h" +#include "../translator/lingvatranslate/lingvatranslate.h" +#include "../translator/stub/stub.h" using namespace bot::settings; -void process_database_channels(std::shared_ptr database, bot::settings::guild *guild, std::vector *webhookIds) +void process_database_channels(std::shared_ptr database, bot::settings::guild &guild, std::vector &webhookIds) { - const std::vector db_channels = database->get_channels(guild->id); + const std::vector db_channels = database->get_channels(guild.id); for (auto db_channel_id = db_channels.begin(); db_channel_id != db_channels.end(); db_channel_id++) { bool channel_found = false; - for (auto channel = guild->channel.begin(); channel != guild->channel.end(); channel++) { + for (auto channel = guild.channel.begin(); channel != guild.channel.end(); channel++) { if (channel->id == *db_channel_id) { - const bot::settings::channel db_channel = database->get_channel(guild->id, channel->id); + const bot::settings::channel db_channel = database->get_channel(guild.id, channel->id); if (!db_channel.source.empty()) channel->source = db_channel.source; for (auto db_target = db_channel.targets.begin(); db_target != db_channel.targets.end(); db_target++) { @@ -39,14 +43,14 @@ void process_database_channels(std::shared_ptr database for (auto target = channel->targets.begin(); target != channel->targets.end(); target++) { if (target->target == db_target->target) { target->webhook = db_target->webhook; - webhookIds->push_back(db_target->webhook.id); + webhookIds.push_back(db_target->webhook.id); target_found = true; break; } } if (!target_found) { channel->targets.push_back(*db_target); - webhookIds->push_back(db_target->webhook.id); + webhookIds.push_back(db_target->webhook.id); } } channel_found = true; @@ -54,23 +58,23 @@ void process_database_channels(std::shared_ptr database } } if (!channel_found) { - const bot::settings::channel db_channel = database->get_channel(guild->id, *db_channel_id); - guild->channel.push_back(db_channel); + const bot::settings::channel db_channel = database->get_channel(guild.id, *db_channel_id); + guild.channel.push_back(db_channel); for (auto db_target = db_channel.targets.begin(); db_target != db_channel.targets.end(); db_target++) - webhookIds->push_back(db_target->webhook.id); + webhookIds.push_back(db_target->webhook.id); } } } -void process_database(std::shared_ptr database, std::vector *guilds, std::vector *webhookIds) +void process_database(std::shared_ptr database, std::vector &guilds, std::vector &webhookIds) { std::cout << "[Launch] Loading database..." << std::endl; const std::vector db_guilds = database->get_guilds(); for (auto db_guild_id = db_guilds.begin(); db_guild_id != db_guilds.end(); db_guild_id++) { bool guild_found = false; - for (auto guild = guilds->begin(); guild != guilds->end(); guild++) { + for (auto guild = guilds.begin(); guild != guilds.end(); guild++) { if (guild->id == *db_guild_id) { - process_database_channels(database, &*guild, webhookIds); + process_database_channels(database, *guild, webhookIds); guild_found = true; break; } @@ -78,13 +82,13 @@ void process_database(std::shared_ptr database, std::ve if (!guild_found) { bot::settings::guild guild; guild.id = *db_guild_id; - process_database_channels(database, &guild, webhookIds); - guilds->push_back(std::move(guild)); + process_database_channels(database, guild, webhookIds); + guilds.push_back(std::move(guild)); } } } -void process_guild_settings(const dpp::json &json, std::vector *guilds, std::vector *webhookIds) +void process_guild_settings(const dpp::json &json, std::vector &guilds, std::vector &webhookIds) { for (auto json_guild = json.begin(); json_guild != json.end(); json_guild++) { if (json_guild->is_object()) { @@ -128,7 +132,7 @@ void process_guild_settings(const dpp::json &json, std::vector *guilds, s target target; target.target = *json_channel_target; target.webhook = dpp::webhook(json_channel->at("webhook")); - webhookIds->push_back(target.webhook.id); + webhookIds.push_back(target.webhook.id); channel.targets.push_back(std::move(target)); } else if (json_channel_target->is_object()) { @@ -136,7 +140,7 @@ void process_guild_settings(const dpp::json &json, std::vector *guilds, s target target; target.target = json_target.key(); target.webhook = dpp::webhook(*json_target); - webhookIds->push_back(target.webhook.id); + webhookIds.push_back(target.webhook.id); channel.targets.push_back(std::move(target)); } } @@ -146,7 +150,7 @@ void process_guild_settings(const dpp::json &json, std::vector *guilds, s guild.channel.push_back(std::move(channel)); } } - guilds->push_back(std::move(guild)); + guilds.push_back(std::move(guild)); } } } @@ -162,103 +166,162 @@ void process_preflang_settings(const dpp::json &json, std::vector * } } -void process_user_settings(const dpp::json &json, uint16_t *avatar_size) -{ - auto json_avatar_size = json.find("avatar_size"); - if (json_avatar_size != json.end()) { - *avatar_size = *json_avatar_size; - if (*avatar_size < 16) - *avatar_size = 16; - else if (*avatar_size > 4096) - *avatar_size = 4096; - } -} - -void process_url(const std::string &url, translator *translator) +void process_server_url(const std::string &url, translator &translator) { std::string_view url_v = url; if (url_v.substr(0, 7) == "http://") { - translator->tls = false; - if (!translator->port) - translator->port = 80; + translator.tls = false; + if (!translator.port) + translator.port = 80; url_v = url_v.substr(7); } else if (url_v.substr(0, 8) == "https://") { - translator->tls = true; - if (!translator->port) - translator->port = 443; + translator.tls = true; + if (!translator.port) + translator.port = 443; url_v = url_v.substr(8); } else { - translator->tls = false; - if (!translator->port) - translator->port = 80; + translator.tls = false; + if (!translator.port) + translator.port = 80; } auto slash_pos = url_v.find_first_of('/'); if (slash_pos != std::string_view::npos) { - translator->url = url_v.substr(slash_pos); + translator.url = url_v.substr(slash_pos); url_v = url_v.substr(0, slash_pos); } else { - translator->url = "/"; + translator.url = "/"; url_v = url_v.substr(0, slash_pos); } - auto colon_pos = url_v.find_first_of(':'); + // We don't have IPv6 support here yet + auto colon_pos = url_v.find_last_of(':'); if (colon_pos != std::string_view::npos) { - translator->hostname = url_v.substr(0, colon_pos); + translator.hostname = url_v.substr(0, colon_pos); const int port = std::stoi(std::string(url_v.substr(colon_pos + 1))); if (port > 0 && port < 65536) - translator->port = static_cast(port); + translator.port = static_cast(port); else throw std::invalid_argument("Port is out of range"); } else { - translator->hostname = url_v; + translator.hostname = url_v; } } -bool process_translator_settings(const dpp::json &json, translator *translator) +bool process_server(const dpp::json &json, translator &translator) +{ + auto json_hostname = json.find("hostname"); + if (json_hostname != json.end()) + translator.hostname = *json_hostname; + + auto json_tls = json.find("tls"); + if (json_tls != json.end()) + translator.tls = *json_tls; + else + translator.tls = false; + + auto json_port = json.find("port"); + if (json_port != json.end()) + translator.port = *json_port; + else + translator.port = 0; + + auto json_url = json.find("url"); + if (json_url == json.end()) { + std::cerr << "[Error] Value url not found in translator object" << std::endl; + return false; + } + if (translator.hostname.empty()) + process_server_url(*json_url, translator); + else + translator.url = *json_url; + + auto json_apiKey = json.find("apiKey"); + if (json_apiKey != json.end()) + translator.apiKey = *json_apiKey; + + return true; +} + +void process_user_settings(const dpp::json &json, uint16_t &avatar_size) +{ + auto json_avatar_size = json.find("avatar_size"); + if (json_avatar_size != json.end()) { + avatar_size = *json_avatar_size; + if (avatar_size < 16) + avatar_size = 16; + else if (avatar_size > 4096) + avatar_size = 4096; + } +} + +bool process_translator_settings(const dpp::json &json, std::shared_ptr &translator_instance) { if (!json.is_object()) { std::cerr << "[Error] Value translator needs to be a object" << std::endl; return false; } - auto json_translate_hostname = json.find("hostname"); - if (json_translate_hostname != json.end()) - translator->hostname = *json_translate_hostname; - else - translator->hostname = {}; - - auto json_translate_tls = json.find("tls"); - if (json_translate_tls != json.end()) - translator->tls = *json_translate_tls; - else - translator->tls = false; - - auto json_translate_port = json.find("port"); - if (json_translate_port != json.end()) - translator->port = *json_translate_port; - else - translator->port = 0; - - auto json_translate_url = json.find("url"); - if (json_translate_url == json.end()) { - std::cerr << "[Error] Value url not found in translator object" << std::endl; - return false; - } - if (translator->hostname.empty()) { - process_url(*json_translate_url, translator); + bot::settings::translator translator; + auto json_translator_type = json.find("type"); + if (json_translator_type != json.end()) { + translator.type = *json_translator_type; + std::transform(translator.type.begin(), translator.type.end(), translator.type.begin(), ::tolower); } else { - translator->url = *json_translate_url; + translator.type = "libretranslate"; } - auto json_translate_apiKey = json.find("apiKey"); - if (json_translate_apiKey != json.end()) - translator->apiKey = *json_translate_apiKey; - else - translator->apiKey.clear(); + if (translator.type == "deepl") { + auto json_deepl_hostname = json.find("hostname"); + if (json_deepl_hostname != json.end()) + translator.hostname = *json_deepl_hostname; + else + translator.hostname = "api-free.deepl.com"; + + auto json_deepl_apiKey = json.find("apiKey"); + if (json_deepl_apiKey == json.end()) { + std::cerr << "[Error] DeepL requires API key for authorization" << std::endl; + return false; + } + translator.apiKey = *json_deepl_apiKey; + + translator_instance = std::make_shared(translator.hostname, translator.apiKey); + } + else if (translator.type == "mozhi") { + if (!process_server(json, translator)) + return false; + + std::string mozhi_engine; + auto json_mozhi_engine = json.find("engine"); + if (json_mozhi_engine != json.end()) + mozhi_engine = *json_mozhi_engine; + else + mozhi_engine = "google"; + + translator_instance = std::make_shared(translator.hostname, translator.port, translator.url, translator.tls, mozhi_engine); + } + else if (translator.type == "libretranslate") { + if (!process_server(json, translator)) + return false; + + translator_instance = std::make_shared(translator.hostname, translator.port, translator.url, translator.tls, translator.apiKey); + } + else if (translator.type == "lingvatranslate") { + if (!process_server(json, translator)) + return false; + + translator_instance = std::make_shared(translator.hostname, translator.port, translator.url, translator.tls); + } + else if (translator.type == "stub") { + translator_instance = std::make_shared(); + } + else { + std::cerr << "[Error] Translator " << translator.type << " is unknown" << std::endl; + return false; + } return true; } @@ -300,6 +363,27 @@ void settings::add_translatebot_webhook(dpp::snowflake webhook_id) m_webhookIds.push_back(webhook_id); } +void settings::erase_channel(guild &guild, dpp::snowflake channel_id) +{ + for (auto channel = guild.channel.begin(); channel != guild.channel.end(); channel++) { + if (channel->id == channel_id) { + guild.channel.erase(channel); + return; + } + } +} + +void settings::erase_guild(dpp::snowflake guild_id) +{ + const std::lock_guard guard(m_mutex); + for (auto guild = m_guilds.begin(); guild != m_guilds.end(); guild++) { + if (guild->id == guild_id) { + m_guilds.erase(guild); + return; + } + } +} + void settings::erase_translatebot_webhook(dpp::snowflake webhook_id) { const std::lock_guard guard(m_mutex); @@ -314,9 +398,9 @@ uint16_t settings::avatar_size() return m_avatarSize; } -channel* settings::get_channel(guild *guild, dpp::snowflake channel_id) +channel* settings::get_channel(guild &guild, dpp::snowflake channel_id) { - for (auto channel = guild->channel.begin(); channel != guild->channel.end(); channel++) { + for (auto channel = guild.channel.begin(); channel != guild.channel.end(); channel++) { if (channel->id == channel_id) return &*channel; } @@ -413,10 +497,10 @@ std::shared_ptr settings::get_database() const return m_database; } -std::unique_ptr settings::get_translator() const +std::shared_ptr settings::get_translator() const { const std::lock_guard guard(m_mutex); - return std::make_unique(m_translator.hostname, m_translator.port, m_translator.url, m_translator.tls, m_translator.apiKey); + return m_translator; } const std::string settings::token() const @@ -446,7 +530,7 @@ bool settings::parse(const std::string &data, bool initialize) try { dpp::json json; try { - json = dpp::json::parse(data); + json = dpp::json::parse(data, nullptr, true, true); } catch (const std::exception &exception) { std::cerr << "[Exception] " << exception.what() << std::endl; @@ -454,21 +538,24 @@ bool settings::parse(const std::string &data, bool initialize) return false; } + const std::lock_guard guard(m_mutex); auto json_token = json.find("token"); - if (json_token == json.end()) { - std::cerr << "[Error] Value token not found" << std::endl; + if (json_token != json.end()) + m_token = *json_token; + else if (char *token = getenv("DTRANSLATEBOT_TOKEN")) + m_token = token; + + if (m_token.empty()) { + std::cerr << "[Error] Discord Bot Token is not configured" << std::endl; return false; } - const std::lock_guard guard(m_mutex); - m_token = *json_token; - std::filesystem::path storage_path; auto json_storage = json.find("storage"); if (json_storage != json.end()) storage_path = std::string(*json_storage); else if (char *storagepath = getenv("DTRANSLATEBOT_STORAGE")) - storage_path = storagepath; + storage_path = storagepath; if (storage_path.empty()) storage_path = std::filesystem::current_path(); @@ -480,14 +567,12 @@ bool settings::parse(const std::string &data, bool initialize) std::cerr << "[Error] Value translator not found" << std::endl; return false; } - if (!process_translator_settings(*json_translator, &m_translator)) + if (!process_translator_settings(*json_translator, m_translator)) return false; - m_avatarSize = 256; - auto json_guilds = json.find("guilds"); if (json_guilds != json.end() && json_guilds->is_object()) - process_guild_settings(*json_guilds, &m_guilds, &m_webhookIds); + process_guild_settings(*json_guilds, m_guilds, m_webhookIds); auto json_preflangs = json.find("preferred_lang"); if (json_preflangs != json.end() && json_preflangs->is_array()) @@ -495,9 +580,9 @@ bool settings::parse(const std::string &data, bool initialize) auto json_user = json.find("user"); if (json_user != json.end() && json_user->is_object()) - process_user_settings(*json_user, &m_avatarSize); + process_user_settings(*json_user, m_avatarSize); - process_database(m_database, &m_guilds, &m_webhookIds); + process_database(m_database, m_guilds, m_webhookIds); return true; } diff --git a/src/settings.h b/src/core/settings.h similarity index 81% rename from src/settings.h rename to src/core/settings.h index 7f51f16..f71f812 100644 --- a/src/settings.h +++ b/src/core/settings.h @@ -19,9 +19,9 @@ #ifndef SETTINGS_H #define SETTINGS_H #include -#include "database_core.h" +#include "database.h" #include "settings_types.h" -#include "translator_core.h" +#include "translator.h" namespace bot { namespace settings { @@ -33,11 +33,13 @@ namespace bot { void add_translatebot_webhook(dpp::snowflake webhook_id); /* erase functions */ + static void erase_channel(guild &guild, dpp::snowflake channel_id); + void erase_guild(dpp::snowflake guild_id); void erase_translatebot_webhook(dpp::snowflake webhook_id); /* get functions */ uint16_t avatar_size(); - static channel* get_channel(guild *guild, dpp::snowflake channel_id); + static channel* get_channel(guild &guild, dpp::snowflake channel_id); channel* get_channel(dpp::snowflake guild_id, dpp::snowflake channel_id); guild* get_guild(dpp::snowflake guild_id); target* get_target(dpp::snowflake guild_id, dpp::snowflake channel_id, const std::string &target); @@ -45,7 +47,7 @@ namespace bot { static const target* get_target(const channel *channel, const std::string &target); const std::vector preferred_languages() const; std::shared_ptr get_database() const; - std::unique_ptr get_translator() const; + std::shared_ptr get_translator() const; const std::string token() const; /* is functions */ @@ -59,14 +61,19 @@ namespace bot { bool parse(const std::string &data, bool initialize = true); bool parse_file(const std::string &filename, bool initialize = true); + /* prevent copies */ + settings() = default; + settings(const settings&) = delete; + settings& operator=(const settings&) = delete; + private: mutable std::recursive_mutex m_mutex; - size_t m_externallyLockedCount; - uint16_t m_avatarSize; + size_t m_externallyLockedCount = 0; + uint16_t m_avatarSize = 256; std::shared_ptr m_database; std::vector m_guilds; std::vector m_prefLangs; - bot::settings::translator m_translator; + std::shared_ptr m_translator; std::string m_token; std::vector m_webhookIds; }; diff --git a/src/settings_types.h b/src/core/settings_types.h similarity index 98% rename from src/settings_types.h rename to src/core/settings_types.h index e1c5b3c..96d9017 100644 --- a/src/settings_types.h +++ b/src/core/settings_types.h @@ -41,6 +41,7 @@ namespace bot { std::vector channel; }; struct translator { + std::string type; std::string hostname; uint16_t port; std::string url; diff --git a/src/slashcommands.cpp b/src/core/slashcommands.cpp similarity index 70% rename from src/slashcommands.cpp rename to src/core/slashcommands.cpp index 2dede96..a55614c 100644 --- a/src/slashcommands.cpp +++ b/src/core/slashcommands.cpp @@ -18,19 +18,20 @@ #include #include "slashcommands.h" +using bot::slashcommands; using namespace std::string_literals; -void bot::slashcommands::process_command_event(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event) +void slashcommands::process_command_event(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event) { if (event.command.get_command_name() == "edit") - bot::slashcommands::process_edit_command(bot, settings, event); + slashcommands::process_edit_command(bot, settings, event); else if (event.command.get_command_name() == "list") - bot::slashcommands::process_list_command(bot, settings, event); + slashcommands::process_list_command(bot, settings, event); else if (event.command.get_command_name() == "translate" || event.command.get_command_name() == "translate_pref") - bot::slashcommands::process_translate_command(bot, settings, event); + slashcommands::process_translate_command(bot, settings, event); } -void bot::slashcommands::process_edit_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event) +void slashcommands::process_edit_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event) { try { dpp::permission user_permissions = event.command.get_resolved_permission(event.command.usr.id); @@ -40,65 +41,84 @@ void bot::slashcommands::process_edit_command(dpp::cluster *bot, bot::settings:: dpp::command_interaction interaction = event.command.get_command_interaction(); if (interaction.options[0].name == "delete") { const std::lock_guard guard(*settings); - if (bot::settings::channel *channel = settings->get_channel(event.command.guild_id, event.command.channel_id)) { - const std::string target = std::get(event.get_parameter("target")); + if (bot::settings::guild *guild = settings->get_guild(event.command.guild_id)) { + if (bot::settings::channel *channel = settings->get_channel(*guild, event.command.channel_id)) { + const std::string target = std::get(event.get_parameter("target")); - std::shared_ptr database = settings->get_database(); - const bot::settings::channel db_channel = database->get_channel(event.command.guild_id, event.command.channel_id); + auto database = settings->get_database(); + const bot::settings::channel db_channel = database->get_channel(event.command.guild_id, event.command.channel_id); - if (db_channel.targets.empty()) { - event.reply(dpp::message("The current channel has no deleteable targets!").set_flags(dpp::m_ephemeral)); - } - else if (target == "**") { - std::vector targets; - for (auto db_target = db_channel.targets.begin(); db_target != db_channel.targets.end(); db_target++) { - targets.push_back(db_target->target); + if (db_channel.targets.empty()) { + event.reply(dpp::message("The current channel has no deleteable targets!").set_flags(dpp::m_ephemeral)); } - for (auto target = channel->targets.begin(); target != channel->targets.end();) { - if (std::find(targets.begin(), targets.end(), target->target) != targets.end()) { - bot->delete_webhook(target->webhook.id, std::bind(&bot::slashcommands::process_deleted_webhook, settings, target->webhook.id, std::placeholders::_1)); - target = channel->targets.erase(target); + else if (target == "**") { + std::vector targets; + for (auto db_target = db_channel.targets.begin(); db_target != db_channel.targets.end(); db_target++) { + targets.push_back(db_target->target); } - else { - target++; + for (auto target = channel->targets.begin(); target != channel->targets.end();) { + if (std::find(targets.begin(), targets.end(), target->target) != targets.end()) { + bot->delete_webhook(target->webhook.id, std::bind(&slashcommands::process_deleted_webhook, settings, target->webhook.id, std::placeholders::_1)); + target = channel->targets.erase(target); + } + else { + target++; + } } + + database->delete_channel(event.command.guild_id, event.command.channel_id); + database->sync(); + + if (channel->targets.empty()) { + settings->erase_channel(*guild, event.command.channel_id); + if (guild->channel.empty()) { + settings->erase_guild(event.command.guild_id); + } + } + + event.reply(dpp::message("Deleteable targets have being deleted!").set_flags(dpp::m_ephemeral)); } - - database->delete_channel(event.command.guild_id, event.command.channel_id); - database->sync(); - - event.reply(dpp::message("Deleteable targets have being deleted!").set_flags(dpp::m_ephemeral)); - } - else { - bool target_found = false; - for (auto db_target = db_channel.targets.begin(); db_target != db_channel.targets.end(); db_target++) { - if (db_target->target == target) { - target_found = true; - break; - } - } - - if (target_found) { - for (auto _target = channel->targets.begin(); _target != channel->targets.end(); _target++) { - if (_target->target == target) { - bot->delete_webhook(_target->webhook.id, std::bind(&bot::slashcommands::process_deleted_webhook, settings, _target->webhook.id, std::placeholders::_1)); - channel->targets.erase(_target); + else { + bool target_found = false; + for (auto db_target = db_channel.targets.begin(); db_target != db_channel.targets.end(); db_target++) { + if (db_target->target == target) { + target_found = true; break; } } - if (db_channel.targets.size() == 1) - database->delete_channel(event.command.guild_id, event.command.channel_id); - else - database->delete_channel_target(event.command.guild_id, event.command.channel_id, target); - database->sync(); + if (target_found) { + for (auto _target = channel->targets.begin(); _target != channel->targets.end(); _target++) { + if (_target->target == target) { + bot->delete_webhook(_target->webhook.id, std::bind(&slashcommands::process_deleted_webhook, settings, _target->webhook.id, std::placeholders::_1)); + channel->targets.erase(_target); + break; + } + } - event.reply(dpp::message("Target have being deleted!").set_flags(dpp::m_ephemeral)); - } - else { - event.reply(dpp::message("Target language is not being found or deleteable!").set_flags(dpp::m_ephemeral)); + if (db_channel.targets.size() == 1) + database->delete_channel(event.command.guild_id, event.command.channel_id); + else + database->delete_channel_target(event.command.guild_id, event.command.channel_id, target); + database->sync(); + + if (channel->targets.empty()) { + settings->erase_channel(*guild, event.command.channel_id); + if (guild->channel.empty()) { + settings->erase_guild(event.command.guild_id); + } + } + + event.reply(dpp::message("Target have being deleted!").set_flags(dpp::m_ephemeral)); + } + else { + event.reply(dpp::message("Target language is not being found or deleteable!").set_flags(dpp::m_ephemeral)); + } } } + else { + event.reply(dpp::message("The current channel is not being translated!").set_flags(dpp::m_ephemeral)); + } } else { event.reply(dpp::message("The current channel is not being translated!").set_flags(dpp::m_ephemeral)); @@ -117,13 +137,13 @@ void bot::slashcommands::process_edit_command(dpp::cluster *bot, bot::settings:: source_valid = true; break; } - language_codes << " " << language.code; + language_codes << ' ' << language.code; } if (source_valid) { channel->source = source; - std::shared_ptr database = settings->get_database(); + auto database = settings->get_database(); database->set_channel_source(event.command.guild_id, event.command.channel_id, source); database->sync(); @@ -147,7 +167,7 @@ void bot::slashcommands::process_edit_command(dpp::cluster *bot, bot::settings:: } } -void bot::slashcommands::process_deleted_webhook(bot::settings::settings *settings, dpp::snowflake webhook_id, const dpp::confirmation_callback_t &callback) +void slashcommands::process_deleted_webhook(bot::settings::settings *settings, dpp::snowflake webhook_id, const dpp::confirmation_callback_t &callback) { if (callback.is_error()) { std::cerr << "[Error] Failed to delete Webhook " << webhook_id << std::endl; @@ -156,7 +176,7 @@ void bot::slashcommands::process_deleted_webhook(bot::settings::settings *settin settings->erase_translatebot_webhook(webhook_id); } -void bot::slashcommands::process_list_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event) +void slashcommands::process_list_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event) { try { dpp::command_interaction interaction = event.command.get_command_interaction(); @@ -171,7 +191,7 @@ void bot::slashcommands::process_list_command(dpp::cluster *bot, bot::settings:: // We want give more information to users who can Manage Webhooks dpp::permission user_permissions = event.command.get_resolved_permission(event.command.usr.id); if (user_permissions.has(dpp::p_manage_webhooks)) { - std::shared_ptr database = settings->get_database(); + auto database = settings->get_database(); const bot::settings::channel db_channel = database->get_channel(event.command.guild_id, event.command.channel_id); for (auto target = channel->targets.begin(); target != channel->targets.end(); target++) { @@ -217,17 +237,44 @@ void bot::slashcommands::process_list_command(dpp::cluster *bot, bot::settings:: event.reply(dpp::message("The current guild have no translated channel!").set_flags(dpp::m_ephemeral)); } } + else if (interaction.options[0].name == "languages") { + dpp::permission user_permissions = event.command.get_resolved_permission(event.command.usr.id); + if (user_permissions.has(dpp::p_manage_webhooks)) { + const std::vector languages = settings->get_translator()->get_languages(); + std::ostringstream reply_languages; + reply_languages << "**Available Languages**\n"; + for (auto language = languages.begin(); language != languages.end();) { + reply_languages << language->name << ": " << language->code; + if (++language != languages.end()) + reply_languages << '\n'; + } + if (reply_languages.str().length() <= 2000) { + event.reply(dpp::message(reply_languages.str()).set_flags(dpp::m_ephemeral)); + } + else { + reply_languages.str({}); + reply_languages << "Available Languages:"; + for (auto language = languages.begin(); language != languages.end(); language++) { + reply_languages << ' ' << language->code; + } + event.reply(dpp::message(reply_languages.str()).set_flags(dpp::m_ephemeral)); + } + } + else { + event.reply(dpp::message("Unauthorized to list available languages!").set_flags(dpp::m_ephemeral)); + } + } else { throw std::invalid_argument("Option " + interaction.options[0].name + " is not known"); } } - catch (const std::exception& exception) { + catch (const std::exception &exception) { std::cerr << "[Exception] " << exception.what() << std::endl; event.reply(dpp::message("Exception while processing command:\n"s + exception.what()).set_flags(dpp::m_ephemeral)); } } -void bot::slashcommands::process_translate_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event) +void slashcommands::process_translate_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event) { try { dpp::permission user_permissions = event.command.get_resolved_permission(event.command.usr.id); @@ -241,13 +288,13 @@ void bot::slashcommands::process_translate_command(dpp::cluster *bot, bot::setti 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"))); + 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(); + const auto languages = settings->get_translator()->get_languages(); std::ostringstream language_codes; bool source_valid = false, target_valid = false; @@ -258,7 +305,7 @@ void bot::slashcommands::process_translate_command(dpp::cluster *bot, bot::setti target_valid = true; if (source_valid && target_valid) break; - language_codes << " " << language.code; + language_codes << ' ' << language.code; } if (source_valid && target_valid) { @@ -271,7 +318,7 @@ void bot::slashcommands::process_translate_command(dpp::cluster *bot, bot::setti webhook.guild_id = channel->guild_id; webhook.name = "Translate Bot Webhook <" + std::to_string(event.command.channel_id) + ":" + source + ":" + target + ">"; - bot->create_webhook(webhook, std::bind(&bot::slashcommands::process_translate_webhook_new_channel, settings, event, source, target, std::placeholders::_1)); + bot->create_webhook(webhook, std::bind(&slashcommands::process_translate_webhook_new_channel, settings, event, source, target, std::placeholders::_1)); } else if (dpp::webhook *webhook = std::get_if(&v_target)) { const bot::settings::target s_target = { target, *webhook }; @@ -280,7 +327,7 @@ void bot::slashcommands::process_translate_command(dpp::cluster *bot, bot::setti settings->add_channel(s_channel, event.command.guild_id); settings->add_translatebot_webhook(webhook->id); - std::shared_ptr database = settings->get_database(); + auto database = settings->get_database(); database->set_channel_source(event.command.guild_id, event.command.channel_id, source); database->add_channel_target(event.command.guild_id, event.command.channel_id, s_target); database->sync(); @@ -298,7 +345,7 @@ void bot::slashcommands::process_translate_command(dpp::cluster *bot, bot::setti webhook.guild_id = channel->guild_id; webhook.name = "Translate Bot Webhook <" + std::to_string(event.command.channel_id) + ":" + source + ":" + target + ">"; - bot->create_webhook(webhook, std::bind(&bot::slashcommands::process_translate_webhook_add_target, settings, event, target, std::placeholders::_1)); + bot->create_webhook(webhook, std::bind(&slashcommands::process_translate_webhook_add_target, settings, event, target, std::placeholders::_1)); } else if (dpp::webhook *webhook = std::get_if(&v_target)) { const bot::settings::target s_target = { target, *webhook }; @@ -306,7 +353,7 @@ void bot::slashcommands::process_translate_command(dpp::cluster *bot, bot::setti settings->add_target(s_target, event.command.guild_id, event.command.channel_id); settings->add_translatebot_webhook(webhook->id); - std::shared_ptr database = settings->get_database(); + auto database = settings->get_database(); database->add_channel_target(event.command.guild_id, event.command.channel_id, s_target); database->sync(); @@ -333,7 +380,7 @@ void bot::slashcommands::process_translate_command(dpp::cluster *bot, bot::setti } } -void bot::slashcommands::process_translate_webhook_add_target(bot::settings::settings *settings, const dpp::slashcommand_t &event, const std::string &target, const dpp::confirmation_callback_t &callback) +void slashcommands::process_translate_webhook_add_target(bot::settings::settings *settings, const dpp::slashcommand_t &event, const std::string &target, const dpp::confirmation_callback_t &callback) { if (callback.is_error()) { event.reply(dpp::message("Failed to generate webhook!").set_flags(dpp::m_ephemeral)); @@ -347,14 +394,14 @@ void bot::slashcommands::process_translate_webhook_add_target(bot::settings::set settings->add_target(s_target, event.command.guild_id, event.command.channel_id); settings->add_translatebot_webhook(webhook.id); - std::shared_ptr database = settings->get_database(); + auto database = settings->get_database(); database->add_channel_target(event.command.guild_id, event.command.channel_id, s_target); database->sync(); event.reply(dpp::message("Channel will be now translated!").set_flags(dpp::m_ephemeral)); } -void bot::slashcommands::process_translate_webhook_new_channel(bot::settings::settings *settings, const dpp::slashcommand_t &event, const std::string &source, const std::string &target, const dpp::confirmation_callback_t &callback) +void slashcommands::process_translate_webhook_new_channel(bot::settings::settings *settings, const dpp::slashcommand_t &event, const std::string &source, const std::string &target, const dpp::confirmation_callback_t &callback) { if (callback.is_error()) { event.reply(dpp::message("Failed to generate webhook!").set_flags(dpp::m_ephemeral)); @@ -369,7 +416,7 @@ void bot::slashcommands::process_translate_webhook_new_channel(bot::settings::se settings->add_channel(s_channel, event.command.guild_id); settings->add_translatebot_webhook(webhook.id); - std::shared_ptr database = settings->get_database(); + auto database = settings->get_database(); database->set_channel_source(event.command.guild_id, event.command.channel_id, source); database->add_channel_target(event.command.guild_id, event.command.channel_id, s_target); database->sync(); @@ -377,7 +424,7 @@ void bot::slashcommands::process_translate_webhook_new_channel(bot::settings::se event.reply(dpp::message("Channel will be now translated!").set_flags(dpp::m_ephemeral)); } -void bot::slashcommands::register_commands(dpp::cluster *bot, bot::settings::settings *settings) +void slashcommands::register_commands(dpp::cluster *bot, bot::settings::settings *settings) { settings->lock(); const std::vector languages = settings->get_translator()->get_languages(); @@ -387,9 +434,9 @@ void bot::slashcommands::register_commands(dpp::cluster *bot, bot::settings::set std::vector commands; dpp::command_option source_option(dpp::co_string, "source", "Source language (ISO 639-1)", true); - source_option.set_max_length(static_cast(2)).set_min_length(static_cast(2)); + source_option.set_max_length(static_cast(5)).set_min_length(static_cast(2)); dpp::command_option target_option(dpp::co_string, "target", "Target language (ISO 639-1)", true); - target_option.set_max_length(static_cast(2)).set_min_length(static_cast(2)); + target_option.set_max_length(static_cast(5)).set_min_length(static_cast(2)); dpp::slashcommand command_edit("edit", "Edit current channel settings", bot->me.id); command_edit.set_default_permissions(dpp::p_manage_webhooks); @@ -404,8 +451,10 @@ void bot::slashcommands::register_commands(dpp::cluster *bot, bot::settings::set dpp::slashcommand command_list("list", "List translation settings", bot->me.id); dpp::command_option channel_list_subcommand(dpp::co_sub_command, "channel", "List current channel translation settings"); dpp::command_option guild_list_subcommand(dpp::co_sub_command, "guild", "List current guild translation settings"); + dpp::command_option languages_list_subcommand(dpp::co_sub_command, "languages", "List available languages to translate"); command_list.add_option(channel_list_subcommand); command_list.add_option(guild_list_subcommand); + command_list.add_option(languages_list_subcommand); commands.push_back(command_list); dpp::slashcommand command_translate("translate", "Translate current channel", bot->me.id); diff --git a/src/slashcommands.h b/src/core/slashcommands.h similarity index 73% rename from src/slashcommands.h rename to src/core/slashcommands.h index abcb0be..a31d37f 100644 --- a/src/slashcommands.h +++ b/src/core/slashcommands.h @@ -23,16 +23,20 @@ #include "settings.h" namespace bot { - namespace slashcommands { - extern void process_command_event(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event); - extern void process_edit_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event); - extern void process_deleted_webhook(bot::settings::settings *settings, dpp::snowflake webhook_id, const dpp::confirmation_callback_t &callback); - extern void process_list_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event); - extern void process_translate_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event); - extern void process_translate_webhook_add_target(bot::settings::settings *settings, const dpp::slashcommand_t &event, const std::string &target, const dpp::confirmation_callback_t &callback); - extern void process_translate_webhook_new_channel(bot::settings::settings *settings, const dpp::slashcommand_t &event, const std::string &source, const std::string &target, const dpp::confirmation_callback_t &callback); - extern void register_commands(dpp::cluster *bot, bot::settings::settings *settings); - } + class slashcommands { + public: + slashcommands() = delete; + static void process_command_event(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event); + static void register_commands(dpp::cluster *bot, bot::settings::settings *settings); + + private: + static void process_edit_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event); + static void process_deleted_webhook(bot::settings::settings *settings, dpp::snowflake webhook_id, const dpp::confirmation_callback_t &callback); + static void process_list_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event); + static void process_translate_command(dpp::cluster *bot, bot::settings::settings *settings, const dpp::slashcommand_t &event); + static void process_translate_webhook_add_target(bot::settings::settings *settings, const dpp::slashcommand_t &event, const std::string &target, const dpp::confirmation_callback_t &callback); + static void process_translate_webhook_new_channel(bot::settings::settings *settings, const dpp::slashcommand_t &event, const std::string &source, const std::string &target, const dpp::confirmation_callback_t &callback); + }; } #endif // SLASHCOMMANDS_H diff --git a/src/submit_queue.cpp b/src/core/submit_queue.cpp similarity index 98% rename from src/submit_queue.cpp rename to src/core/submit_queue.cpp index e752fab..12ce874 100644 --- a/src/submit_queue.cpp +++ b/src/core/submit_queue.cpp @@ -19,7 +19,7 @@ #include #include "submit_queue.h" #include "webhook_push.h" -using namespace bot; +using bot::submit_queue; using namespace std::chrono_literals; void submit_queue::add(const translated_message &message) diff --git a/src/submit_queue.h b/src/core/submit_queue.h similarity index 100% rename from src/submit_queue.h rename to src/core/submit_queue.h diff --git a/src/translator_core.cpp b/src/core/translator.cpp similarity index 94% rename from src/translator_core.cpp rename to src/core/translator.cpp index d32d544..993e198 100644 --- a/src/translator_core.cpp +++ b/src/core/translator.cpp @@ -19,17 +19,9 @@ #ifndef NDEBUG #include #endif -#include "translator_core.h" +#include "translator.h" using namespace bot::translator; -translator::translator() -{ -} - -translator::~translator() -{ -} - const std::vector translator::get_languages() { #ifndef NDEBUG diff --git a/src/translator_core.h b/src/core/translator.h similarity index 74% rename from src/translator_core.h rename to src/core/translator.h index 33fbf0e..91502d9 100644 --- a/src/translator_core.h +++ b/src/core/translator.h @@ -16,9 +16,10 @@ * responsible for anything with use of the software, you are self responsible. *****************************************************************************/ -#ifndef TRANSLATOR_CORE_H -#define TRANSLATOR_CORE_H +#ifndef TRANSLATOR_H +#define TRANSLATOR_H +#include #include #include @@ -29,14 +30,21 @@ namespace bot { std::string name; }; + struct supported_languages { + std::vector languages; + std::chrono::system_clock::time_point query_time; + }; + class translator { public: - explicit translator(); - virtual ~translator(); + explicit translator() = default; + virtual ~translator() = default; + translator(const translator&) = delete; + translator& operator=(const translator&) = delete; virtual const std::vector get_languages(); virtual const std::string translate(const std::string &text, const std::string &source, const std::string &target); }; } } -#endif // TRANSLATOR_CORE_H +#endif // TRANSLATOR_H diff --git a/src/webhook_push.cpp b/src/core/webhook_push.cpp similarity index 83% rename from src/webhook_push.cpp rename to src/core/webhook_push.cpp index 1e4a0f7..457ec3e 100644 --- a/src/webhook_push.cpp +++ b/src/core/webhook_push.cpp @@ -25,10 +25,12 @@ using namespace std::string_view_literals; void bot::webhook_push::run(const bot::translated_message &message, dpp::cluster *bot) { dpp::json json_body = { - {"username"s, message.author}, - {"avatar_url"s, message.avatar} + {"username"s, message.author} }; + if (!message.avatar.empty()) + json_body["avatar_url"] = message.avatar; + // We will split too long messages into multiple messages if (message.message.length() > 2000) { std::string_view message_v = message.message; @@ -73,14 +75,18 @@ void bot::webhook_push::run(const bot::translated_message &message, dpp::cluster } } +void webhook_request_completed(std::promise &promise, dpp::json &json, const dpp::http_request_completion_t &event) +{ + if (event.status != 204) + std::cerr << "[Warning] Webhook push returned unexpected code " << event.status << std::endl; + promise.set_value(event); +} + void bot::webhook_push::push_request(dpp::snowflake webhook_id, const std::string &webhook_token, const std::string &json, dpp::cluster *bot) { - std::promise _p; - std::future _f = _p.get_future(); - bot->post_rest(API_PATH "/webhooks", std::to_string(webhook_id), dpp::utility::url_encode(webhook_token), dpp::m_post, json, [&bot, &_p](dpp::json &json, const dpp::http_request_completion_t &event) { - if (event.status != 204) - std::cerr << "[Warning] Webhook push returned unexpected code " << event.status << std::endl; - _p.set_value(event); - }); - _f.wait(); + std::promise promise; + std::future future = promise.get_future(); + bot->post_rest(API_PATH "/webhooks", std::to_string(webhook_id), dpp::utility::url_encode(webhook_token), dpp::m_post, json, + std::bind(&webhook_request_completed, std::ref(promise), std::placeholders::_1, std::placeholders::_2)); + future.wait(); } diff --git a/src/webhook_push.h b/src/core/webhook_push.h similarity index 97% rename from src/webhook_push.h rename to src/core/webhook_push.h index 643b20d..d4b03f9 100644 --- a/src/webhook_push.h +++ b/src/core/webhook_push.h @@ -26,6 +26,7 @@ namespace bot { class webhook_push { public: + webhook_push() = delete; static void run(const bot::translated_message &message, dpp::cluster *bot); private: diff --git a/src/database_file.cpp b/src/database/file/file.cpp similarity index 95% rename from src/database_file.cpp rename to src/database/file/file.cpp index db5774b..e7c6a6d 100644 --- a/src/database_file.cpp +++ b/src/database/file/file.cpp @@ -27,7 +27,7 @@ #include #include #include -#include "database_file.h" +#include "file.h" using namespace bot::database; using namespace std::string_literals; @@ -98,7 +98,7 @@ void file::add_channel_target(dpp::snowflake guild_id, dpp::snowflake channel_id } bot::settings::channel channel; - cache_get_channel(channel_id, &channel); + cache_get_channel(channel_id, channel); channel.targets.push_back(target); cache_add_channel(guild_id, channel_id); guild->channel.push_back(std::move(channel)); @@ -107,7 +107,7 @@ void file::add_channel_target(dpp::snowflake guild_id, dpp::snowflake channel_id } bot::settings::channel channel; - cache_get_channel(channel_id, &channel); + cache_get_channel(channel_id, channel); channel.targets.push_back(target); cache_add_channel(guild_id, channel_id); m_dataCache.push_back({ guild_id, { std::move(channel) } }); @@ -141,7 +141,7 @@ void file::delete_channel(dpp::snowflake guild_id, dpp::snowflake channel_id) } std::vector channels; - cache_guild(guild_id, &channels); + cache_guild(guild_id, channels); for (auto channel = channels.begin(); channel != channels.end(); channel++) { if (*channel == channel_id) { channels.erase(channel); @@ -175,7 +175,7 @@ void file::delete_channel_target(dpp::snowflake guild_id, dpp::snowflake channel } bot::settings::channel channel; - cache_get_channel(channel_id, &channel); + cache_get_channel(channel_id, channel); for (auto _target = channel.targets.begin(); _target != channel.targets.end(); _target++) { if (_target->target == target) { channel.targets.erase(_target); @@ -188,7 +188,7 @@ void file::delete_channel_target(dpp::snowflake guild_id, dpp::snowflake channel } bot::settings::channel channel; - cache_get_channel(channel_id, &channel); + cache_get_channel(channel_id, channel); for (auto _target = channel.targets.begin(); _target != channel.targets.end(); _target++) { if (_target->target == target) { channel.targets.erase(_target); @@ -266,7 +266,7 @@ bot::settings::channel file::get_channel(dpp::snowflake guild_id, dpp::snowflake } bot::settings::channel channel; - cache_get_channel(channel_id, &channel); + cache_get_channel(channel_id, channel); return channel; } @@ -281,7 +281,7 @@ std::vector file::get_channels(dpp::snowflake guild_id) } std::vector channels; - cache_guild(guild_id, &channels); + cache_guild(guild_id, channels); return channels; } @@ -330,7 +330,7 @@ std::vector file::get_guilds() { const std::lock_guard guard(m_mutex); std::vector guilds; - list_guilds(&guilds); + list_guilds(guilds); return guilds; } @@ -348,7 +348,7 @@ void file::set_channel_source(dpp::snowflake guild_id, dpp::snowflake channel_id } bot::settings::channel channel; - cache_get_channel(channel_id, &channel); + cache_get_channel(channel_id, channel); channel.source = source; cache_add_channel(guild_id, channel_id); guild->channel.push_back(std::move(channel)); @@ -357,7 +357,7 @@ void file::set_channel_source(dpp::snowflake guild_id, dpp::snowflake channel_id } bot::settings::channel channel; - cache_get_channel(channel_id, &channel); + cache_get_channel(channel_id, channel); channel.source = source; cache_add_channel(guild_id, channel_id); m_dataCache.push_back({ guild_id, { std::move(channel) } }); @@ -390,16 +390,16 @@ void file::cache_add_channel(dpp::snowflake guild_id, dpp::snowflake channel_id) } std::vector channels; - cache_guild(guild_id, &channels); + cache_guild(guild_id, channels); if (std::find(channels.begin(), channels.end(), channel_id) == channels.end()) channels.push_back(channel_id); m_channelCache.push_back({ guild_id, std::move(channels) }); } -void file::cache_get_channel(dpp::snowflake channel_id, bot::settings::channel *channel) +void file::cache_get_channel(dpp::snowflake channel_id, settings::channel &channel) { - channel->id = channel_id; + channel.id = channel_id; const std::filesystem::path channel_file = m_storagePath / "channel" / (std::to_string(channel_id) + ".json"); @@ -418,7 +418,7 @@ void file::cache_get_channel(dpp::snowflake channel_id, bot::settings::channel * if (json.is_object()) { auto json_channel_source = json.find("source"); if (json_channel_source != json.end()) - channel->source = *json_channel_source; + channel.source = *json_channel_source; auto json_channel_target = json.find("target"); if (json_channel_target != json.end()) { @@ -433,7 +433,7 @@ void file::cache_get_channel(dpp::snowflake channel_id, bot::settings::channel * else if (json_target->is_string()) { target.webhook = dpp::webhook(*json_target); } - channel->targets.push_back(std::move(target)); + channel.targets.push_back(std::move(target)); } } } @@ -444,7 +444,7 @@ void file::cache_get_channel(dpp::snowflake channel_id, bot::settings::channel * } } -void file::cache_guild(dpp::snowflake guild_id, std::vector *channels) +void file::cache_guild(dpp::snowflake guild_id, std::vector &channels) { const std::filesystem::path guild_file = m_storagePath / "guild" / (std::to_string(guild_id) + ".json"); @@ -463,9 +463,9 @@ void file::cache_guild(dpp::snowflake guild_id, std::vector *cha if (json.is_array()) { for (auto channel = json.begin(); channel != json.end(); channel++) { if (channel->is_number()) - channels->push_back(*channel); + channels.push_back(*channel); else if (channel->is_string()) - channels->push_back(std::stoull(std::string(*channel))); + channels.push_back(std::stoull(std::string(*channel))); } } } @@ -474,7 +474,7 @@ void file::cache_guild(dpp::snowflake guild_id, std::vector *cha } } -void file::list_guilds(std::vector *guilds) +void file::list_guilds(std::vector &guilds) { const std::filesystem::path guild_dir = m_storagePath / "guild"; @@ -488,7 +488,7 @@ void file::list_guilds(std::vector *guilds) if (std::all_of(guild_filename.begin(), guild_filename.end(), ::isdigit)) { try { dpp::snowflake guild_id = std::stoull(guild_filename); - guilds->push_back(guild_id); + guilds.push_back(guild_id); } catch (const std::exception &exception) { std::cerr << "[Exception] " << exception.what() << std::endl; diff --git a/src/database_file.h b/src/database/file/file.h similarity index 95% rename from src/database_file.h rename to src/database/file/file.h index 5947b3f..f34bd4d 100644 --- a/src/database_file.h +++ b/src/database/file/file.h @@ -26,7 +26,7 @@ #define WIN32_LEAN_AND_MEAN #include #endif -#include "database_core.h" +#include "../../core/database.h" namespace bot { namespace database { @@ -53,9 +53,9 @@ namespace bot { private: void cache_add_channel(dpp::snowflake guild_id, dpp::snowflake channel_id); - void cache_get_channel(dpp::snowflake channel_id, bot::settings::channel *channel); - void cache_guild(dpp::snowflake guild_id, std::vector *channels); - void list_guilds(std::vector *guilds); + void cache_get_channel(dpp::snowflake channel_id, bot::settings::channel &channel); + void cache_guild(dpp::snowflake guild_id, std::vector &channels); + void list_guilds(std::vector &guilds); void sync_cache(); #if defined(__unix__) int fd; diff --git a/src/resources/win32/dtranslatebot.rc.in b/src/resources/win32/dtranslatebot.rc.in new file mode 100644 index 0000000..d1dc7ce --- /dev/null +++ b/src/resources/win32/dtranslatebot.rc.in @@ -0,0 +1,29 @@ +#include +VS_VERSION_INFO VERSIONINFO +FILEVERSION @dtranslatebot_VERSION_MAJOR@, @dtranslatebot_VERSION_MINOR@, @dtranslatebot_VERSION_PATCH@, 0 +PRODUCTVERSION @dtranslatebot_VERSION_MAJOR@, @dtranslatebot_VERSION_MINOR@, @dtranslatebot_VERSION_PATCH@, 0 +FILEFLAGSMASK 0x3fL +FILEFLAGS 0 +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0, 1200 + END + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Syping" + VALUE "FileDescription", "Discord Translation Bot" + VALUE "FileVersion", "@dtranslatebot_VERSION@" + VALUE "InternalName", "dtranslatebot" + VALUE "LegalCopyright", "Copyright © 2023-2024 Syping" + VALUE "OriginalFilename", "dtranslatebot.exe" + VALUE "ProductName", "dtranslatebot" + VALUE "ProductVersion", "@dtranslatebot_VERSION@" + END + END +END diff --git a/src/systemd/dtranslatebot.service.in b/src/systemd/dtranslatebot.service.in new file mode 100644 index 0000000..6c2e0c9 --- /dev/null +++ b/src/systemd/dtranslatebot.service.in @@ -0,0 +1,13 @@ +[Unit] +Description=Discord Translation Bot +After=network.target + +[Service] +User=dtranslatebot +Group=dtranslatebot +WorkingDirectory=@dtranslatebot_SERVICE_WORKDIR@ +ExecStart="@CMAKE_INSTALL_FULL_BINDIR@/dtranslatebot" "@CMAKE_INSTALL_FULL_SYSCONFDIR@/dtranslatebot.json" +Restart=on-abnormal + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/dtranslatebot.sysusersd.in b/src/systemd/dtranslatebot.sysusersd.in new file mode 100644 index 0000000..144004d --- /dev/null +++ b/src/systemd/dtranslatebot.sysusersd.in @@ -0,0 +1 @@ +u dtranslatebot - "Discord Translation Bot" "@CMAKE_INSTALL_FULL_LOCALSTATEDIR@/lib/dtranslatebot" - diff --git a/src/translator/deepl/deepl.cpp b/src/translator/deepl/deepl.cpp new file mode 100644 index 0000000..e81481c --- /dev/null +++ b/src/translator/deepl/deepl.cpp @@ -0,0 +1,116 @@ +/***************************************************************************** +* 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 +#include "deepl.h" +using namespace bot::translator; +using namespace std::chrono_literals; +using namespace std::string_literals; + +deepl::deepl(const std::string &hostname, const std::string apiKey) : + m_hostname(hostname), m_apiKey(apiKey) +{ +} + +deepl::~deepl() +{ +} + +const std::vector deepl::get_languages() +{ + if (!m_languages.languages.empty()) { + auto current_time = std::chrono::system_clock::now(); + auto threshold_time = m_languages.query_time + 24h; + if (current_time <= threshold_time) + return m_languages.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()) { + m_languages.languages.clear(); + for (auto json_language = response.begin(); json_language != response.end(); json_language++) { + if (json_language->is_object()) { + language language; + + auto json_lang_code = json_language->find("language"); + if (json_lang_code != json_language->end()) + language.code = *json_lang_code; + + if (language.code.size() > 2) + std::transform(language.code.begin(), language.code.begin() + 2, language.code.begin(), ::tolower); + else + std::transform(language.code.begin(), language.code.end(), language.code.begin(), ::tolower); + + auto json_lang_name = json_language->find("name"); + if (json_lang_name != json_language->end()) + language.name = *json_lang_name; + + if (!language.code.empty() && !language.name.empty()) + m_languages.languages.push_back(std::move(language)); + } + } + m_languages.query_time = std::chrono::system_clock::now(); + } + } + } + catch (const std::exception &exception) { + std::cerr << "[Exception] " << exception.what() << std::endl; + } + + return m_languages.languages; +} + +const std::string deepl::translate(const std::string &text, const std::string &source, const std::string &target) +{ + const dpp::http_headers http_headers = { + {"Authorization"s, "DeepL-Auth-Key " + m_apiKey}, + {"Content-Type"s, "application/json"s} + }; + + dpp::json json_body = { + {"text"s, { text } }, + {"source_lang"s, source}, + {"target_lang"s, target}, + }; + + 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()) { + for (auto translation = translations->begin(); translation != translations->end(); translation++) { + auto tr_text = translation->find("text"); + if (tr_text != translation->end()) + return *tr_text; + } + } + } + } + } + catch (const std::exception &exception) { + std::cerr << "[Exception] " << exception.what() << std::endl; + } + + return text; +} diff --git a/src/translator/deepl/deepl.h b/src/translator/deepl/deepl.h new file mode 100644 index 0000000..b4e0d0d --- /dev/null +++ b/src/translator/deepl/deepl.h @@ -0,0 +1,43 @@ +/***************************************************************************** +* 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 TRANSLATOR_DEEPL_H +#define TRANSLATOR_DEEPL_H + +#include +#include "../../core/translator.h" + +namespace bot { + namespace translator { + class deepl : public translator { + public: + explicit deepl(const std::string &hostname, const std::string apiKey = {}); + ~deepl() override; + const std::vector get_languages() override; + 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; + }; + } +} + +#endif // TRANSLATOR_DEEPL_H diff --git a/src/translator_libretranslate.cpp b/src/translator/libretranslate/libretranslate.cpp similarity index 69% rename from src/translator_libretranslate.cpp rename to src/translator/libretranslate/libretranslate.cpp index 78a0026..3d82a89 100644 --- a/src/translator_libretranslate.cpp +++ b/src/translator/libretranslate/libretranslate.cpp @@ -18,8 +18,9 @@ #include #include -#include "translator_libretranslate.h" +#include "libretranslate.h" using namespace bot::translator; +using namespace std::chrono_literals; using namespace std::string_literals; libretranslate::libretranslate(const std::string &hostname, uint16_t port, const std::string &url, bool tls, const std::string apiKey) : @@ -33,29 +34,36 @@ libretranslate::~libretranslate() const std::vector libretranslate::get_languages() { - std::vector languages; + if (!m_languages.languages.empty()) { + auto current_time = std::chrono::system_clock::now(); + auto threshold_time = m_languages.query_time + 24h; + if (current_time <= threshold_time) + return m_languages.languages; + } try { - dpp::https_client http_request(m_hostname, m_port, m_url + "languages", "GET", {}, {}, !m_tls); + 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()) { - for (const auto &json_language : response) { - if (json_language.is_object()) { + m_languages.languages.clear(); + for (auto json_language = response.begin(); json_language != response.end(); json_language++) { + if (json_language->is_object()) { language language; - auto json_lang_code = json_language.find("code"); - if (json_lang_code != json_language.end()) + auto json_lang_code = json_language->find("code"); + if (json_lang_code != json_language->end()) language.code = *json_lang_code; - auto json_lang_name = json_language.find("name"); - if (json_lang_name != json_language.end()) + auto json_lang_name = json_language->find("name"); + if (json_lang_name != json_language->end()) language.name = *json_lang_name; if (!language.code.empty() && !language.name.empty()) - languages.push_back(std::move(language)); + m_languages.languages.push_back(std::move(language)); } } + m_languages.query_time = std::chrono::system_clock::now(); } } } @@ -63,7 +71,7 @@ const std::vector libretranslate::get_languages() std::cerr << "[Exception] " << exception.what() << std::endl; } - return languages; + return m_languages.languages; } const std::string libretranslate::translate(const std::string &text, const std::string &source, const std::string &target) @@ -83,7 +91,7 @@ const std::string libretranslate::translate(const std::string &text, const std:: 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); + 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()) { diff --git a/src/translator_libretranslate.h b/src/translator/libretranslate/libretranslate.h similarity index 92% rename from src/translator_libretranslate.h rename to src/translator/libretranslate/libretranslate.h index eaa287d..75306a8 100644 --- a/src/translator_libretranslate.h +++ b/src/translator/libretranslate/libretranslate.h @@ -20,12 +20,13 @@ #define TRANSLATOR_LIBRETRANSLATE_H #include -#include -#include "translator_core.h" +#include +#include "../../core/translator.h" namespace bot { namespace translator { class libretranslate : public translator { + public: explicit libretranslate(const std::string &hostname, uint16_t port, const std::string &url, bool tls, const std::string apiKey = {}); ~libretranslate() override; @@ -33,8 +34,10 @@ 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; uint16_t m_port; std::string m_url; bool m_tls; diff --git a/src/translator/lingvatranslate/lingvatranslate.cpp b/src/translator/lingvatranslate/lingvatranslate.cpp new file mode 100644 index 0000000..b8352e5 --- /dev/null +++ b/src/translator/lingvatranslate/lingvatranslate.cpp @@ -0,0 +1,99 @@ +/***************************************************************************** +* 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 +#include +#include "lingvatranslate.h" +using namespace bot::translator; +using namespace std::chrono_literals; +using namespace std::string_literals; + +lingvatranslate::lingvatranslate(const std::string &hostname, uint16_t port, const std::string &url, bool tls) : + m_hostname(hostname), m_port(port), m_url(url), m_tls(tls) +{ +} + +lingvatranslate::~lingvatranslate() +{ +} + +const std::vector lingvatranslate::get_languages() +{ + if (!m_languages.languages.empty()) { + auto current_time = std::chrono::system_clock::now(); + auto threshold_time = m_languages.query_time + 24h; + if (current_time <= threshold_time) + return m_languages.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()) { + m_languages.languages.clear(); + for (auto json_language = languages->begin(); json_language != languages->end(); json_language++) { + if (json_language->is_object()) { + language language; + + auto json_lang_code = json_language->find("code"); + if (json_lang_code != json_language->end()) + language.code = *json_lang_code; + + auto json_lang_name = json_language->find("name"); + if (json_lang_name != json_language->end()) + language.name = *json_lang_name; + + if (!language.code.empty() && !language.name.empty()) + m_languages.languages.push_back(std::move(language)); + } + } + m_languages.query_time = std::chrono::system_clock::now(); + } + } + } + } + catch (const std::exception &exception) { + std::cerr << "[Exception] " << exception.what() << std::endl; + } + + return m_languages.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()) + return *tr_text; + } + } + } + catch (const std::exception &exception) { + std::cerr << "[Exception] " << exception.what() << std::endl; + } + + return text; +} diff --git a/src/translator/lingvatranslate/lingvatranslate.h b/src/translator/lingvatranslate/lingvatranslate.h new file mode 100644 index 0000000..805dd9b --- /dev/null +++ b/src/translator/lingvatranslate/lingvatranslate.h @@ -0,0 +1,47 @@ +/***************************************************************************** +* 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 TRANSLATOR_LINGVATRANSLATE_H +#define TRANSLATOR_LINGVATRANSLATE_H + +#include +#include +#include "../../core/translator.h" + +namespace bot { + namespace translator { + class lingvatranslate : public translator { + + public: + explicit lingvatranslate(const std::string &hostname, uint16_t port, const std::string &url, bool tls); + ~lingvatranslate() override; + const std::vector get_languages() override; + 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; + std::string m_url; + bool m_tls; + }; + } +} + +#endif // TRANSLATOR_LINGVATRANSLATE_H diff --git a/src/translator/mozhi/mozhi.cpp b/src/translator/mozhi/mozhi.cpp new file mode 100644 index 0000000..e5e8ea1 --- /dev/null +++ b/src/translator/mozhi/mozhi.cpp @@ -0,0 +1,105 @@ +/***************************************************************************** +* 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 +#include +#include "mozhi.h" +using namespace bot::translator; +using namespace std::chrono_literals; +using namespace std::string_literals; + +mozhi::mozhi(const std::string &hostname, uint16_t port, const std::string &url, bool tls, const std::string &engine) : + m_hostname(hostname), m_port(port), m_url(url), m_tls(tls), m_engine(engine) +{ +} + +mozhi::~mozhi() +{ +} + +const std::vector mozhi::get_languages() +{ + if (!m_languages.languages.empty()) { + auto current_time = std::chrono::system_clock::now(); + auto threshold_time = m_languages.query_time + 24h; + if (current_time <= threshold_time) + return m_languages.languages; + } + + try { + 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()) { + m_languages.languages.clear(); + for (auto json_language = response.begin(); json_language != response.end(); json_language++) { + if (json_language->is_object()) { + language language; + + auto json_lang_code = json_language->find("Id"); + if (json_lang_code != json_language->end()) + language.code = *json_lang_code; + + auto json_lang_name = json_language->find("Name"); + if (json_lang_name != json_language->end()) + language.name = *json_lang_name; + + if (!language.code.empty() && !language.name.empty()) + m_languages.languages.push_back(std::move(language)); + } + } + m_languages.query_time = std::chrono::system_clock::now(); + } + } + } + catch (const std::exception &exception) { + std::cerr << "[Exception] " << exception.what() << std::endl; + } + + return m_languages.languages; +} + +const std::string mozhi::translate(const std::string &text, const std::string &source, const std::string &target) +{ + try { + const std::string parameters = dpp::utility::make_url_parameters({ + {"engine"s, m_engine}, + {"from"s, source}, + {"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()) + return *tr_text; + } + } + } + catch (const std::exception &exception) { + std::cerr << "[Exception] " << exception.what() << std::endl; + } + + return text; +} diff --git a/src/translator/mozhi/mozhi.h b/src/translator/mozhi/mozhi.h new file mode 100644 index 0000000..8a8735a --- /dev/null +++ b/src/translator/mozhi/mozhi.h @@ -0,0 +1,48 @@ +/***************************************************************************** +* 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 TRANSLATOR_MOZHI_H +#define TRANSLATOR_MOZHI_H + +#include +#include +#include "../../core/translator.h" + +namespace bot { + namespace translator { + class mozhi : public translator { + + public: + explicit mozhi(const std::string &hostname, uint16_t port, const std::string &url, bool tls, const std::string &engine); + ~mozhi() override; + const std::vector get_languages() override; + 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; + uint16_t m_port; + std::string m_url; + bool m_tls; + }; + } +} + +#endif // TRANSLATOR_MOZHI_H diff --git a/src/translator/stub/stub.cpp b/src/translator/stub/stub.cpp new file mode 100644 index 0000000..7e7a083 --- /dev/null +++ b/src/translator/stub/stub.cpp @@ -0,0 +1,39 @@ +/***************************************************************************** +* 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 "stub.h" +using namespace bot::translator; +using namespace std::string_literals; + +stub::stub() +{ +} + +stub::~stub() +{ +} + +const std::vector stub::get_languages() +{ + return { {"a*", "Any Language"} }; +} + +const std::string stub::translate(const std::string &text, [[maybe_unused]] const std::string &source, [[maybe_unused]] const std::string &target) +{ + return text; +} diff --git a/src/translator/stub/stub.h b/src/translator/stub/stub.h new file mode 100644 index 0000000..c6c73e4 --- /dev/null +++ b/src/translator/stub/stub.h @@ -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. +*****************************************************************************/ + +#ifndef TRANSLATOR_STUB_H +#define TRANSLATOR_STUB_H + +#include "../../core/translator.h" + +namespace bot { + namespace translator { + class stub : public translator { + public: + explicit stub(); + ~stub() override; + const std::vector get_languages() override; + const std::string translate(const std::string &text, const std::string &source, const std::string &target) override; + }; + } +} + +#endif // TRANSLATOR_STUB_H