diff --git a/lib/pleroma/web/akkoma_compat_controller.ex b/lib/pleroma/web/akkoma_compat_controller.ex
new file mode 100644
index 0000000000..57abc29d05
--- /dev/null
+++ b/lib/pleroma/web/akkoma_compat_controller.ex
@@ -0,0 +1,100 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/api_spec/operations/akkoma_compat_operation.ex b/lib/pleroma/web/api_spec/operations/akkoma_compat_operation.ex
new file mode 100644
index 0000000000..83d29caf9b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/akkoma_compat_operation.ex
@@ -0,0 +1,91 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 81ee1a0a6f..8d0171bf3d 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -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
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 9bb3e07ba9..8fcabcd20d 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -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)
diff --git a/test/pleroma/web/akkoma_compat_controller_test.exs b/test/pleroma/web/akkoma_compat_controller_test.exs
new file mode 100644
index 0000000000..02b691ca0f
--- /dev/null
+++ b/test/pleroma/web/akkoma_compat_controller_test.exs
@@ -0,0 +1,60 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# 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