Support Akkoma translation routes

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-09-08 15:40:42 +02:00
parent b09152801a
commit efbd25d0b5
5 changed files with 263 additions and 1 deletions

View file

@ -0,0 +1,100 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AkkomaCompatController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.Language.Translation
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(:skip_auth when action == :translation_languages)
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]} when action == :translate
)
plug(Pleroma.Web.ApiSpec.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AkkomaCompatOperation
@doc "GET /api/v1/akkoma/translation/languages"
def translation_languages(conn, _params) do
with {:enabled, true} <- {:enabled, Translation.configured?()},
{:ok, source_languages} <- Translation.supported_languages(:source),
{:ok, target_languages} <- Translation.supported_languages(:target) do
source_languages =
source_languages
|> Enum.map(fn lang -> %{code: lang, name: lang} end)
target_languages =
target_languages
|> Enum.map(fn lang -> %{code: lang, name: lang} end)
conn
|> json(%{source: source_languages, target: target_languages})
else
{:enabled, false} ->
json(conn, %{})
e ->
{:error, e}
end
end
@doc "GET /api/v1/statuses/:id/translations/:language"
def translate(
%{
assigns: %{user: user},
private: %{open_api_spex: %{params: %{id: status_id} = params}}
} = conn,
_
) do
with {:authentication, true} <-
{:authentication,
!is_nil(user) ||
Pleroma.Config.get([Translation, :allow_unauthenticated])},
%Activity{object: object} <- Activity.get_by_id_with_object(status_id),
{:visibility, visibility} when visibility in ["public", "unlisted"] <-
{:visibility, Visibility.get_visibility(object)},
{:allow_remote, true} <-
{:allow_remote,
Object.local?(object) ||
Pleroma.Config.get([Translation, :allow_remote])},
{:language, language} when is_binary(language) <-
{:language, Map.get(params, :language) || user.language},
{:ok, result} <-
Translation.translate(
object.data["content"],
object.data["language"],
language
) do
json(conn, %{detected_language: result.detected_source_language, text: result.content})
else
{:authentication, false} ->
render_error(conn, :unauthorized, "Authorization is required to translate statuses")
{:allow_remote, false} ->
render_error(conn, :bad_request, "You can't translate remote posts")
{:language, nil} ->
render_error(conn, :bad_request, "Language not specified")
{:visibility, _} ->
render_error(conn, :not_found, "Record not found")
{:error, :not_found} ->
render_error(conn, :not_found, "Translation service not configured")
{:error, error} when error in [:unexpected_response, :quota_exceeded, :too_many_requests] ->
render_error(conn, :service_unavailable, "Translation service not available")
nil ->
render_error(conn, :not_found, "Record not found")
end
end
end

View file

@ -0,0 +1,91 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.AkkomaCompatOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
# Adapted from https://akkoma.dev/AkkomaGang/akkoma/src/branch/develop/lib/pleroma/web/api_spec/operations/translate_operation.ex
def translation_languages_operation() do
%Operation{
tags: ["Akkoma compatibility routes"],
summary: "Get translation languages",
description: "Retreieve a list of supported source and target language",
operationId: "AkkomaCompatController.translation_languages",
responses: %{
200 =>
Operation.response(
"Translation languages",
"application/json",
source_dest_languages_schema()
)
}
}
end
defp source_dest_languages_schema do
%Schema{
type: :object,
required: [:source, :target],
properties: %{
source: languages_schema(),
target: languages_schema()
}
}
end
defp languages_schema do
%Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
code: %Schema{type: :string},
name: %Schema{type: :string}
}
}
}
end
def translate_operation() do
%Operation{
tags: ["Akkoma compatibility routes"],
summary: "Translate status",
description: "Translate status with an external API",
operationId: "AkkomaCompatController.translate",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [
Operation.parameter(:id, :path, FlakeID.schema(), "Status ID",
example: "9umDrYheeY451cQnEe",
required: true
),
Operation.parameter(:language, :path, :string, "Target language code", example: "en"),
Operation.parameter(:from, :query, :string, "Source language code (unused)",
example: "en"
)
],
responses: %{
200 =>
Operation.response(
"Translated status",
"application/json",
%Schema{
type: :object,
required: [:detected_language, :text],
properties: %{
detected_language: %Schema{type: :string},
text: %Schema{type: :string}
}
}
)
}
}
end
end

View file

@ -182,7 +182,11 @@ def features do
end,
"events",
"multitenancy",
"pleroma:bites"
"pleroma:bites",
# Akkoma compatibility
if Pleroma.Language.Translation.configured?() do
"akkoma:machine_translation"
end
]
|> Enum.filter(& &1)
end

View file

@ -718,6 +718,13 @@ defmodule Pleroma.Web.Router do
end
end
scope "/", Pleroma.Web do
pipe_through(:api)
get("/api/v1/akkoma/translation/languages", AkkomaCompatController, :translation_languages)
get("/api/v1/statuses/:id/translations/:language", AkkomaCompatController, :translate)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)

View file

@ -0,0 +1,60 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AkkomaCompatControllerTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
describe "translation_languages" do
test "returns supported languages list", %{conn: conn} do
clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
assert %{
"source" => [%{"code" => "en", "name" => "en"}, %{"code" => "pl", "name" => "pl"}],
"target" => [%{"code" => "en", "name" => "en"}, %{"code" => "pl", "name" => "pl"}]
} =
conn
|> get("/api/v1/akkoma/translation/languages")
|> json_response_and_validate_schema(200)
end
test "returns empty object when disabled", %{conn: conn} do
clear_config([Pleroma.Language.Translation, :provider], nil)
assert %{} ==
conn
|> get("/api/v1/akkoma/translation/languages")
|> json_response(200)
end
end
describe "translate" do
test "it translates a status to given language" do
clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
%{conn: conn} = oauth_access(["read:statuses"])
another_user = insert(:user)
{:ok, activity} =
CommonAPI.post(another_user, %{
status: "Cześć!",
visibility: "public",
language: "pl"
})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/translations/en")
|> json_response_and_validate_schema(200)
assert response == %{
"text" => "!ćśezC",
"detected_language" => "pl"
}
end
end
end