Support Akkoma translation routes
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
b09152801a
commit
efbd25d0b5
5 changed files with 263 additions and 1 deletions
100
lib/pleroma/web/akkoma_compat_controller.ex
Normal file
100
lib/pleroma/web/akkoma_compat_controller.ex
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
60
test/pleroma/web/akkoma_compat_controller_test.exs
Normal file
60
test/pleroma/web/akkoma_compat_controller_test.exs
Normal 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
|
Loading…
Reference in a new issue