From 3580b7810547768d7b8e8e7a51ba5daad6cf35ad Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Tue, 30 Aug 2022 13:54:37 +0100 Subject: [PATCH] allow listing languages supported by a given translator --- lib/pleroma/akkoma/translators/deepl.ex | 30 ++++++++++++- .../akkoma/translators/libre_translate.ex | 23 ++++++++++ lib/pleroma/akkoma/translators/translator.ex | 1 + .../controllers/translation_controller.ex | 43 +++++++++++++++++++ .../operations/translate_operation.ex | 41 ++++++++++++++++++ .../controllers/status_controller.ex | 2 +- lib/pleroma/web/router.ex | 5 +++ test/pleroma/translators/deepl_test.exs | 30 +++++++++++++ .../translators/libre_translate_test.exs | 25 +++++++++++ 9 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 lib/pleroma/web/akkoma_api/controllers/translation_controller.ex create mode 100644 lib/pleroma/web/api_spec/operations/translate_operation.ex diff --git a/lib/pleroma/akkoma/translators/deepl.ex b/lib/pleroma/akkoma/translators/deepl.ex index 0a4a7fe10..881a7c71a 100644 --- a/lib/pleroma/akkoma/translators/deepl.ex +++ b/lib/pleroma/akkoma/translators/deepl.ex @@ -21,6 +21,24 @@ defp tier do Config.get([:deepl, :tier]) end + @impl Pleroma.Akkoma.Translator + def languages do + with {:ok, %{status: 200} = response} <- do_languages(), + {:ok, body} <- Jason.decode(response.body) do + resp = + Enum.map(body, fn %{"language" => code, "name" => name} -> %{code: code, name: name} end) + + {:ok, resp} + else + {:ok, %{status: status} = response} -> + Logger.warning("DeepL: Request rejected: #{inspect(response)}") + {:error, "DeepL request failed (code #{status})"} + + {:error, reason} -> + {:error, reason} + end + end + @impl Pleroma.Akkoma.Translator def translate(string, to_language) do with {:ok, %{status: 200} = response} <- do_request(api_key(), tier(), string, to_language), @@ -45,7 +63,8 @@ defp do_request(api_key, tier, string, to_language) do URI.encode_query( %{ text: string, - target_lang: to_language + target_lang: to_language, + tag_handling: "html" }, :rfc3986 ), @@ -55,4 +74,13 @@ defp do_request(api_key, tier, string, to_language) do ] ) end + + defp do_languages() do + HTTP.get( + base_url(tier()) <> "languages?type=target", + [ + {"authorization", "DeepL-Auth-Key #{api_key()}"} + ] + ) + end end diff --git a/lib/pleroma/akkoma/translators/libre_translate.ex b/lib/pleroma/akkoma/translators/libre_translate.ex index 615d04192..f0a431932 100644 --- a/lib/pleroma/akkoma/translators/libre_translate.ex +++ b/lib/pleroma/akkoma/translators/libre_translate.ex @@ -13,6 +13,22 @@ defp url do Config.get([:libre_translate, :url]) end + @impl Pleroma.Akkoma.Translator + def languages do + with {:ok, %{status: 200} = response} <- do_languages(), + {:ok, body} <- Jason.decode(response.body) do + resp = Enum.map(body, fn %{"code" => code, "name" => name} -> %{code: code, name: name} end) + {:ok, resp} + else + {:ok, %{status: status} = response} -> + Logger.warning("LibreTranslate: Request rejected: #{inspect(response)}") + {:error, "LibreTranslate request failed (code #{status})"} + + {:error, reason} -> + {:error, reason} + end + end + @impl Pleroma.Akkoma.Translator def translate(string, to_language) do with {:ok, %{status: 200} = response} <- do_request(string, to_language), @@ -48,4 +64,11 @@ defp do_request(string, to_language) do ] ) end + + defp do_languages() do + url = URI.parse(url()) + url = %{url | path: "/languages"} + + HTTP.get(to_string(url)) + end end diff --git a/lib/pleroma/akkoma/translators/translator.ex b/lib/pleroma/akkoma/translators/translator.ex index 0276ed6c2..aa1b1a8cc 100644 --- a/lib/pleroma/akkoma/translators/translator.ex +++ b/lib/pleroma/akkoma/translators/translator.ex @@ -1,3 +1,4 @@ defmodule Pleroma.Akkoma.Translator do @callback translate(String.t(), String.t()) :: {:ok, String.t(), String.t()} | {:error, any()} + @callback languages() :: {:ok, [%{name: String.t(), code: String.t()}]} | {:error, any()} end diff --git a/lib/pleroma/web/akkoma_api/controllers/translation_controller.ex b/lib/pleroma/web/akkoma_api/controllers/translation_controller.ex new file mode 100644 index 000000000..49ef89a50 --- /dev/null +++ b/lib/pleroma/web/akkoma_api/controllers/translation_controller.ex @@ -0,0 +1,43 @@ +defmodule Pleroma.Web.AkkomaAPI.TranslationController do + use Pleroma.Web, :controller + + alias Pleroma.Web.Plugs.OAuthScopesPlug + + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + + @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} + plug( + OAuthScopesPlug, + %{@unauthenticated_access | scopes: ["read:statuses"]} + when action in [ + :languages + ] + ) + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TranslationOperation + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + @doc "GET /api/v1/akkoma/translation/languages" + def languages(conn, _params) do + with {:ok, languages} <- get_languages() do + conn + |> json(languages) + else + e -> IO.inspect(e) + end + end + + defp get_languages do + module = Pleroma.Config.get([:translator, :module]) + + @cachex.fetch!(:translations_cache, "languages:#{module}}", fn _ -> + with {:ok, languages} <- module.languages() do + {:ok, languages} + else + {:error, err} -> {:ignore, {:error, err}} + end + end) + end +end diff --git a/lib/pleroma/web/api_spec/operations/translate_operation.ex b/lib/pleroma/web/api_spec/operations/translate_operation.ex new file mode 100644 index 000000000..aa3b69a18 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/translate_operation.ex @@ -0,0 +1,41 @@ +defmodule Pleroma.Web.ApiSpec.TranslationOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + + @spec open_api_operation(atom) :: Operation.t() + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + @spec languages_operation() :: Operation.t() + def languages_operation() do + %Operation{ + tags: ["Retrieve status translation"], + summary: "Translate status", + description: "View the translation of a given status", + operationId: "AkkomaAPI.TranslationController.languages", + security: [%{"oAuth" => ["read:statuses"]}], + responses: %{ + 200 => Operation.response("Translation", "application/json", languages_schema()) + } + } + end + + defp languages_schema do + %Schema{ + type: "array", + items: %Schema{ + type: "object", + properties: %{ + code: %Schema{ + type: "string" + }, + name: %Schema{ + type: "string" + } + } + } + } + end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index d9b93ca5e..09e7daf19 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -450,7 +450,7 @@ def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language}) d end defp fetch_or_translate(status_id, text, language, translation_module) do - @cachex.fetch!(:user_cache, "translations:#{status_id}:#{language}", fn _ -> + @cachex.fetch!(:translations_cache, "translations:#{status_id}:#{language}", fn _ -> value = translation_module.translate(text, language) with {:ok, _, _} <- value do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index aff7b67db..175b1c4c0 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -462,6 +462,11 @@ defmodule Pleroma.Web.Router do put("/statuses/:id/emoji_reactions/:emoji", EmojiReactionController, :create) end + scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do + pipe_through(:authenticated_api) + get("/translation/languages", TranslationController, :languages) + end + scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:authenticated_api) diff --git a/test/pleroma/translators/deepl_test.exs b/test/pleroma/translators/deepl_test.exs index 286d21d3e..4323c8355 100644 --- a/test/pleroma/translators/deepl_test.exs +++ b/test/pleroma/translators/deepl_test.exs @@ -8,6 +8,36 @@ defmodule Pleroma.Akkoma.Translators.DeepLTest do clear_config([:deepl, :api_key], "deepl_api_key") end + test "should list supported languages" do + clear_config([:deepl, :tier], :free) + + Tesla.Mock.mock(fn + %{method: :get, url: "https://api-free.deepl.com/v2/languages?type=target"} = env -> + auth_header = Enum.find(env.headers, fn {k, _v} -> k == "authorization" end) + assert {"authorization", "DeepL-Auth-Key deepl_api_key"} = auth_header + + %Tesla.Env{ + status: 200, + body: + Jason.encode!([ + %{ + "language" => "BG", + "name" => "Bulgarian", + "supports_formality" => false + }, + %{ + "language" => "CS", + "name" => "Czech", + "supports_formality" => false + } + ]) + } + end) + + assert {:ok, [%{code: "BG", name: "Bulgarian"}, %{code: "CS", name: "Czech"}]} = + DeepL.languages() + end + test "should work with the free tier" do clear_config([:deepl, :tier], :free) diff --git a/test/pleroma/translators/libre_translate_test.exs b/test/pleroma/translators/libre_translate_test.exs index 9ed2c5323..ee71eef35 100644 --- a/test/pleroma/translators/libre_translate_test.exs +++ b/test/pleroma/translators/libre_translate_test.exs @@ -8,6 +8,31 @@ defmodule Pleroma.Akkoma.Translators.LibreTranslateTest do clear_config([:libre_translate, :url], "http://libre.translate/translate") end + test "should list supported languages" do + clear_config([:deepl, :tier], :free) + + Tesla.Mock.mock(fn + %{method: :get, url: "http://libre.translate/languages"} = _ -> + %Tesla.Env{ + status: 200, + body: + Jason.encode!([ + %{ + "code" => "en", + "name" => "English" + }, + %{ + "code" => "ar", + "name" => "Arabic" + } + ]) + } + end) + + assert {:ok, [%{code: "en", name: "English"}, %{code: "ar", name: "Arabic"}]} = + LibreTranslate.languages() + end + test "should work without an API key" do Tesla.Mock.mock(fn %{method: :post, url: "http://libre.translate/translate"} = env ->