From 90f91168f7ed9af6a4141fafa11417a6419a0c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 30 Oct 2022 18:52:26 +0100 Subject: [PATCH] Expose translation service availability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- config/description.exs | 13 ++++ lib/pleroma/translation.ex | 54 +++++++++++++ lib/pleroma/translation/deepl.ex | 75 +++++++++++++++++++ lib/pleroma/translation/libretranslate.ex | 66 ++++++++++++++++ lib/pleroma/translation/service.ex | 20 +++++ .../api_spec/operations/status_operation.ex | 6 +- .../web/mastodon_api/views/instance_view.ex | 3 +- 7 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 lib/pleroma/translation.ex create mode 100644 lib/pleroma/translation/deepl.ex create mode 100644 lib/pleroma/translation/libretranslate.ex create mode 100644 lib/pleroma/translation/service.ex diff --git a/config/description.exs b/config/description.exs index 6c13bde31..0efea0882 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3523,5 +3523,18 @@ config :pleroma, :config_description, [ suggestion: [100_000] } ] + }, + %{ + group: :pleroma, + key: Pleroma.Translation, + type: :group, + description: "Translation providers", + children: [ + %{ + key: Pleroma.Translation, + type: :service, + suggestions: [Pleroma.Translation.DeepL, Pleroma.Translation.LibreTranslate] + } + ] } ] diff --git a/lib/pleroma/translation.ex b/lib/pleroma/translation.ex new file mode 100644 index 000000000..112f12cab --- /dev/null +++ b/lib/pleroma/translation.ex @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Translation do + @cache_ttl 86_400_000 + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + + def configured? do + service = get_service() + + !!service and service.configured? + end + + def translate(text, source_language, target_language) do + cache_key = get_cache_key(text, source_language, target_language) + + case @cachex.get(:translations_cache, cache_key) do + {:ok, nil} -> + service = get_service() + + result = + if !service or !service.configured? do + {:error, :not_found} + else + service.translate(text, source_language, target_language) + end + + store_result(result, cache_key) + + result + + {:ok, result} -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + end + + defp get_service, do: Pleroma.Config.get([__MODULE__, :service]) + + defp get_cache_key(text, source_language, target_language) do + "#{source_language}/#{target_language}/#{content_hash(text)}" + end + + defp store_result({:ok, result}, cache_key) do + @cachex.put(:translations_cache, cache_key, result, ttl: @cache_ttl) + end + + defp store_result(_, _), do: nil + + defp content_hash(text), do: :crypto.hash(:sha256, text) |> Base.encode64() +end diff --git a/lib/pleroma/translation/deepl.ex b/lib/pleroma/translation/deepl.ex new file mode 100644 index 000000000..76fff4693 --- /dev/null +++ b/lib/pleroma/translation/deepl.ex @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Translation.DeepL do + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] + + alias Pleroma.Translation.Service + + @behaviour Service + + @impl Service + def configured? do + not_empty_string(get_plan()) and not_empty_string(get_api_key()) + end + + @impl Service + def translate(content, source_language, target_language) do + endpoint = endpoint_url() + + case Pleroma.HTTP.post( + endpoint <> + "?" <> + URI.encode_query(%{ + text: content, + source_lang: source_language |> String.upcase(), + target_lang: target_language, + tag_handling: "html" + }), + "", + [ + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Authorization", "DeepL-Auth-Key #{get_api_key()}"} + ] + ) do + {:ok, %{status: 429}} -> + {:error, :too_many_requests} + + {:ok, %{status: 456}} -> + {:error, :quota_exceeded} + + {:ok, %{status: 200} = res} -> + %{ + "translations" => [ + %{"text" => content, "detected_source_language" => detected_source_language} + ] + } = Jason.decode!(res.body) + + {:ok, + %{ + content: content, + detected_source_language: detected_source_language, + provider: "DeepL" + }} + + _ -> + {:error, :internal_server_error} + end + end + + defp endpoint_url do + case get_plan() do + "free" -> "https://api-free.deepl.com/v2/translate" + _ -> "https://api.deepl.com/v2/translate" + end + end + + defp get_plan do + Pleroma.Config.get([__MODULE__, :plan]) + end + + defp get_api_key do + Pleroma.Config.get([__MODULE__, :api_key]) + end +end diff --git a/lib/pleroma/translation/libretranslate.ex b/lib/pleroma/translation/libretranslate.ex new file mode 100644 index 000000000..049053d43 --- /dev/null +++ b/lib/pleroma/translation/libretranslate.ex @@ -0,0 +1,66 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Translation.LibreTranslate do + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] + + alias Pleroma.Translation.Service + + @behaviour Service + + @impl Service + def configured?, do: not_empty_string(get_base_url()) + + @impl Service + def translate(content, source_language, target_language) do + endpoint = endpoint_url() + + case Pleroma.HTTP.post( + endpoint, + Jason.encode!(%{ + q: content, + source: source_language |> String.upcase(), + target: target_language, + format: "html", + api_key: get_api_key() + }), + [ + {"Content-Type", "application/json"} + ] + ) do + {:ok, %{status: 429}} -> + {:error, :too_many_requests} + + {:ok, %{status: 403}} -> + {:error, :quota_exceeded} + + {:ok, %{status: 200} = res} -> + %{ + "translatedText" => content + } = Jason.decode!(res.body) + + {:ok, + %{ + content: content, + detected_source_language: source_language, + provider: "LibreTranslate" + }} + + _ -> + {:error, :internal_server_error} + end + end + + defp endpoint_url do + get_base_url() <> "/translate" + end + + defp get_base_url do + Pleroma.Config.get([__MODULE__, :base_url]) + end + + defp get_api_key do + Pleroma.Config.get([__MODULE__, :api_key], "") + end +end diff --git a/lib/pleroma/translation/service.ex b/lib/pleroma/translation/service.ex new file mode 100644 index 000000000..55e995e92 --- /dev/null +++ b/lib/pleroma/translation/service.ex @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Translation.Service do + @callback configured?() :: boolean() + + @callback translate( + content :: String.t(), + source_language :: String.t(), + target_language :: String.t() + ) :: + {:ok, + %{ + content: String.t(), + detected_source_language: String.t(), + provider: String.t() + }} + | {:error, atom()} +end diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 961a2f402..00529bc47 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -433,9 +433,9 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do required: false ), responses: %{ - 200 => Operation.response("Translation", "application/json", translation()) - 400 => Operation.response("Error", "application/json", ApiError) - 404 => Operation.response("Error", "application/json", ApiError) + 200 => Operation.response("Translation", "application/json", translation()), + 400 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError), 503 => Operation.response("Error", "application/json", ApiError) } } diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index e4c6c81e1..f48e80fd6 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -202,7 +202,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do }, vapid: %{ public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - } + }, + translation: %{enabled: Pleroma.Translation.configured?} }) end