mirror of
https://git.pleroma.social/pleroma/pleroma.git
synced 2025-04-09 12:34:09 +00:00
Merge remote-tracking branch 'origin/develop' into events
Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
commit
26dcb4f964
34 changed files with 1121 additions and 9 deletions
changelog.d
config
lib/pleroma
application.exapplication_requirements.ex
language
web
test
fixtures
pleroma
language
web
activity_pub/transmogrifier
mastodon_api/controllers
rich_media
support
1
changelog.d/emoji_likes.add
Normal file
1
changelog.d/emoji_likes.add
Normal file
|
@ -0,0 +1 @@
|
|||
Support Mitra-style emoji likes.
|
1
changelog.d/releases.fix
Normal file
1
changelog.d/releases.fix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix release builds
|
1
changelog.d/translate-posts.add
Normal file
1
changelog.d/translate-posts.add
Normal file
|
@ -0,0 +1 @@
|
|||
Support translation providers (DeepL, LibreTranslate)
|
1
changelog.d/truncate-rich-media.change
Normal file
1
changelog.d/truncate-rich-media.change
Normal file
|
@ -0,0 +1 @@
|
|||
Truncate the length of Rich Media title and description fields
|
|
@ -3529,6 +3529,50 @@ config :pleroma, :config_description, [
|
|||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: Pleroma.Language.Translation,
|
||||
type: :group,
|
||||
description: "Translation providers",
|
||||
children: [
|
||||
%{
|
||||
key: :provider,
|
||||
type: :module,
|
||||
suggestions: [
|
||||
Pleroma.Language.Translation.Deepl,
|
||||
Pleroma.Language.Translation.Libretranslate
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: {:subgroup, Pleroma.Language.Translation.Deepl},
|
||||
key: :base_url,
|
||||
label: "DeepL base URL",
|
||||
type: :string,
|
||||
suggestions: ["https://api-free.deepl.com", "https://api.deepl.com"]
|
||||
},
|
||||
%{
|
||||
group: {:subgroup, Pleroma.Language.Translation.Deepl},
|
||||
key: :api_key,
|
||||
label: "DeepL API Key",
|
||||
type: :string,
|
||||
suggestions: ["YOUR_API_KEY"]
|
||||
},
|
||||
%{
|
||||
group: {:subgroup, Pleroma.Language.Translation.Libretranslate},
|
||||
key: :base_url,
|
||||
label: "LibreTranslate instance URL",
|
||||
type: :string,
|
||||
suggestions: ["https://libretranslate.com"]
|
||||
},
|
||||
%{
|
||||
group: {:subgroup, Pleroma.Language.Translation.Libretranslate},
|
||||
key: :api_key,
|
||||
label: "LibreTranslate API Key",
|
||||
type: :string,
|
||||
suggestions: ["YOUR_API_KEY"]
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :geospatial,
|
||||
key: Geospatial.Service,
|
||||
|
|
|
@ -56,7 +56,7 @@ defmodule Pleroma.Application do
|
|||
Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled()
|
||||
end
|
||||
|
||||
if Mix.env() != :test do
|
||||
if Config.get(:env) != :test do
|
||||
Pleroma.ApplicationRequirements.verify!()
|
||||
end
|
||||
|
||||
|
@ -173,7 +173,8 @@ defmodule Pleroma.Application do
|
|||
limit: 500_000
|
||||
),
|
||||
build_cachex("rel_me", limit: 2500),
|
||||
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000)
|
||||
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5_000),
|
||||
build_cachex("translations", default_ttl: :timer.hours(24), limit: 5_000)
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -202,11 +202,24 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
false
|
||||
end
|
||||
|
||||
translation_commands_status =
|
||||
if Pleroma.Language.Translation.missing_dependencies() == [] do
|
||||
true
|
||||
else
|
||||
Logger.error(
|
||||
"The following dependencies required by the currently enabled " <>
|
||||
"translation provider are not installed: " <>
|
||||
inspect(Pleroma.Language.Translation.missing_dependencies())
|
||||
)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
if Enum.all?(
|
||||
[
|
||||
preview_proxy_commands_status,
|
||||
language_detector_commands_status
|
||||
| filter_commands_statuses
|
||||
language_detector_commands_status,
|
||||
translation_commands_status | filter_commands_statuses
|
||||
],
|
||||
& &1
|
||||
) do
|
||||
|
|
127
lib/pleroma/language/translation.ex
Normal file
127
lib/pleroma/language/translation.ex
Normal file
|
@ -0,0 +1,127 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation do
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
def configured? do
|
||||
provider = get_provider()
|
||||
|
||||
!!provider and provider.configured?
|
||||
end
|
||||
|
||||
def missing_dependencies do
|
||||
provider = get_provider()
|
||||
|
||||
if provider do
|
||||
provider.missing_dependencies()
|
||||
else
|
||||
[]
|
||||
end
|
||||
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} ->
|
||||
provider = get_provider()
|
||||
|
||||
result =
|
||||
if !configured?() do
|
||||
{:error, :not_found}
|
||||
else
|
||||
provider.translate(text, source_language, target_language)
|
||||
|> scrub_html()
|
||||
end
|
||||
|
||||
store_result(result, cache_key)
|
||||
|
||||
result
|
||||
|
||||
{:ok, result} ->
|
||||
{:ok, result}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def supported_languages(type) when type in [:source, :target] do
|
||||
provider = get_provider()
|
||||
|
||||
cache_key = "#{type}_languages/#{provider.name()}"
|
||||
|
||||
case @cachex.get(:translations_cache, cache_key) do
|
||||
{:ok, nil} ->
|
||||
result =
|
||||
if !configured?() do
|
||||
{:error, :not_found}
|
||||
else
|
||||
provider.supported_languages(type)
|
||||
end
|
||||
|
||||
store_result(result, cache_key)
|
||||
|
||||
result
|
||||
|
||||
{:ok, result} ->
|
||||
{:ok, result}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def languages_matrix do
|
||||
provider = get_provider()
|
||||
|
||||
cache_key = "languages_matrix/#{provider.name()}"
|
||||
|
||||
case @cachex.get(:translations_cache, cache_key) do
|
||||
{:ok, nil} ->
|
||||
result =
|
||||
if !configured?() do
|
||||
{:error, :not_found}
|
||||
else
|
||||
provider.languages_matrix()
|
||||
end
|
||||
|
||||
store_result(result, cache_key)
|
||||
|
||||
result
|
||||
|
||||
{:ok, result} ->
|
||||
{:ok, result}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_provider, do: Pleroma.Config.get([__MODULE__, :provider])
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
defp store_result(_, _), do: nil
|
||||
|
||||
defp content_hash(text), do: :crypto.hash(:sha256, text) |> Base.encode64()
|
||||
|
||||
defp scrub_html({:ok, %{content: content} = result}) when is_binary(content) do
|
||||
scrubbers = Pleroma.Config.get([:markup, :scrub_policy])
|
||||
|
||||
content
|
||||
|> Pleroma.HTML.filter_tags(scrubbers)
|
||||
|
||||
{:ok, %{result | content: content}}
|
||||
end
|
||||
|
||||
defp scrub_html(result), do: result
|
||||
end
|
123
lib/pleroma/language/translation/deepl.ex
Normal file
123
lib/pleroma/language/translation/deepl.ex
Normal file
|
@ -0,0 +1,123 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.Deepl do
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
alias Pleroma.Language.Translation.Provider
|
||||
|
||||
use Provider
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@name "DeepL"
|
||||
|
||||
@impl Provider
|
||||
def configured?, do: not_empty_string(base_url()) and not_empty_string(api_key())
|
||||
|
||||
@impl Provider
|
||||
def translate(content, source_language, target_language) do
|
||||
endpoint =
|
||||
base_url()
|
||||
|> URI.merge("/v2/translate")
|
||||
|> URI.to_string()
|
||||
|
||||
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 #{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: @name
|
||||
}}
|
||||
|
||||
_ ->
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def supported_languages(type) when type in [:source, :target] do
|
||||
endpoint =
|
||||
base_url()
|
||||
|> URI.merge("/v2/languages")
|
||||
|> URI.to_string()
|
||||
|
||||
case Pleroma.HTTP.post(
|
||||
endpoint <> "?" <> URI.encode_query(%{type: type}),
|
||||
"",
|
||||
[
|
||||
{"Content-Type", "application/x-www-form-urlencoded"},
|
||||
{"Authorization", "DeepL-Auth-Key #{api_key()}"}
|
||||
]
|
||||
) do
|
||||
{:ok, %{status: 200} = res} ->
|
||||
languages =
|
||||
Jason.decode!(res.body)
|
||||
|> Enum.map(fn %{"language" => language} -> language |> String.downcase() end)
|
||||
|> Enum.map(fn language ->
|
||||
if String.contains?(language, "-") do
|
||||
[language, language |> String.split("-") |> Enum.at(0)]
|
||||
else
|
||||
language
|
||||
end
|
||||
end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, languages}
|
||||
|
||||
_ ->
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def languages_matrix do
|
||||
with {:ok, source_languages} <- supported_languages(:source),
|
||||
{:ok, target_languages} <- supported_languages(:target) do
|
||||
{:ok,
|
||||
Map.new(source_languages, fn language -> {language, target_languages -- [language]} end)}
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def name, do: @name
|
||||
|
||||
defp base_url do
|
||||
Pleroma.Config.get([__MODULE__, :base_url])
|
||||
end
|
||||
|
||||
defp api_key do
|
||||
Pleroma.Config.get([__MODULE__, :api_key])
|
||||
end
|
||||
end
|
93
lib/pleroma/language/translation/libretranslate.ex
Normal file
93
lib/pleroma/language/translation/libretranslate.ex
Normal file
|
@ -0,0 +1,93 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.Libretranslate do
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
alias Pleroma.Language.Translation.Provider
|
||||
|
||||
use Provider
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@name "LibreTranslate"
|
||||
|
||||
@impl Provider
|
||||
def configured?, do: not_empty_string(base_url()) and not_empty_string(api_key())
|
||||
|
||||
@impl Provider
|
||||
def translate(content, source_language, target_language) do
|
||||
case Pleroma.HTTP.post(
|
||||
base_url() <> "/translate",
|
||||
Jason.encode!(%{
|
||||
q: content,
|
||||
source: source_language |> String.upcase(),
|
||||
target: target_language,
|
||||
format: "html",
|
||||
api_key: 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: @name
|
||||
}}
|
||||
|
||||
_ ->
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def supported_languages(_) do
|
||||
case Pleroma.HTTP.get(base_url() <> "/languages") do
|
||||
{:ok, %{status: 200} = res} ->
|
||||
languages =
|
||||
Jason.decode!(res.body)
|
||||
|> Enum.map(fn %{"code" => code} -> code end)
|
||||
|
||||
{:ok, languages}
|
||||
|
||||
_ ->
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def languages_matrix do
|
||||
with {:ok, source_languages} <- supported_languages(:source),
|
||||
{:ok, target_languages} <- supported_languages(:target) do
|
||||
{:ok,
|
||||
Map.new(source_languages, fn language -> {language, target_languages -- [language]} end)}
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def name, do: @name
|
||||
|
||||
defp base_url do
|
||||
Pleroma.Config.get([__MODULE__, :base_url])
|
||||
end
|
||||
|
||||
defp api_key do
|
||||
Pleroma.Config.get([__MODULE__, :api_key], "")
|
||||
end
|
||||
end
|
40
lib/pleroma/language/translation/provider.ex
Normal file
40
lib/pleroma/language/translation/provider.ex
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.Provider do
|
||||
alias Pleroma.Language.Translation.Provider
|
||||
|
||||
@callback missing_dependencies() :: [String.t()]
|
||||
|
||||
@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()}
|
||||
|
||||
@callback supported_languages(type :: :string | :target) ::
|
||||
{:ok, [String.t()]} | {:error, atom()}
|
||||
|
||||
@callback languages_matrix() :: {:ok, Map.t()} | {:error, atom()}
|
||||
|
||||
@callback name() :: String.t()
|
||||
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
@impl Provider
|
||||
def missing_dependencies, do: []
|
||||
|
||||
defoverridable missing_dependencies: 0
|
||||
end
|
||||
end
|
||||
end
|
|
@ -492,6 +492,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
}
|
||||
|
||||
# Rewrite misskey likes into EmojiReacts
|
||||
defp handle_incoming_normalized(
|
||||
%{
|
||||
"type" => "Like",
|
||||
"content" => content
|
||||
} = data,
|
||||
options
|
||||
)
|
||||
when is_binary(content) do
|
||||
data
|
||||
|> Map.put("type", "EmojiReact")
|
||||
|> handle_incoming_normalized(options)
|
||||
end
|
||||
|
||||
defp handle_incoming_normalized(
|
||||
%{
|
||||
"type" => "Like",
|
||||
|
@ -500,7 +513,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
options
|
||||
) do
|
||||
data
|
||||
|> Map.put("type", "EmojiReact")
|
||||
|> Map.put("content", @misskey_reactions[reaction] || reaction)
|
||||
|> handle_incoming_normalized(options)
|
||||
end
|
||||
|
|
|
@ -57,6 +57,29 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def translation_languages_operation do
|
||||
%Operation{
|
||||
tags: ["Instance misc"],
|
||||
summary: "Retrieve supported languages matrix",
|
||||
operationId: "InstanceController.translation_languages",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"Translation languages matrix",
|
||||
"application/json",
|
||||
%Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Supported target languages for a source language"
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp instance do
|
||||
%Schema{
|
||||
type: :object,
|
||||
|
|
|
@ -428,6 +428,38 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def translate_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve status information"],
|
||||
summary: "Translate status",
|
||||
description: "Translate status with an external API",
|
||||
operationId: "StatusController.translate",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
parameters: [id_param()],
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
lang: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Translation target language."
|
||||
}
|
||||
}
|
||||
},
|
||||
required: false
|
||||
),
|
||||
responses: %{
|
||||
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)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def favourites_operation do
|
||||
%Operation{
|
||||
tags: ["Timelines"],
|
||||
|
@ -826,4 +858,32 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp translation do
|
||||
%Schema{
|
||||
title: "StatusTranslation",
|
||||
description: "Represents status translation with related information.",
|
||||
type: :object,
|
||||
required: [:content, :detected_source_language, :provider],
|
||||
properties: %{
|
||||
content: %Schema{
|
||||
type: :string,
|
||||
description: "Translated status content"
|
||||
},
|
||||
detected_source_language: %Schema{
|
||||
type: :string,
|
||||
description: "Detected source language"
|
||||
},
|
||||
provider: %Schema{
|
||||
type: :string,
|
||||
description: "Translation provider service name"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"content" => "Software für die nächste Generation der sozialen Medien.",
|
||||
"detected_source_language" => "en",
|
||||
"provider" => "Deepl"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -122,6 +122,10 @@ defmodule Pleroma.Web.Federator do
|
|||
Logger.debug("Unhandled actor #{actor}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
{:reject, reason} = e ->
|
||||
Logger.debug("Rejected by MRF: #{inspect(reason)}")
|
||||
{:error, e}
|
||||
|
||||
e ->
|
||||
# Just drop those for now
|
||||
Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end)
|
||||
|
|
|
@ -30,4 +30,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
|
|||
def rules(conn, _params) do
|
||||
render(conn, "rules.json")
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/instance/translation_languages"
|
||||
def translation_languages(conn, _params) do
|
||||
render(conn, "translation_languages.json")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Bookmark
|
||||
alias Pleroma.BookmarkFolder
|
||||
alias Pleroma.Language.Translation
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.ScheduledActivity
|
||||
|
@ -43,6 +44,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
]
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :translate)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:statuses"]}
|
||||
|
@ -84,7 +87,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
|
||||
)
|
||||
|
||||
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
|
||||
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete translate)a
|
||||
|
||||
plug(
|
||||
RateLimiter,
|
||||
|
@ -552,6 +555,41 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "POST /api/v1/statuses/:id/translate"
|
||||
def translate(
|
||||
%{
|
||||
assigns: %{user: user},
|
||||
private: %{open_api_spex: %{body_params: params, params: %{id: status_id}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with %Activity{object: object} <- Activity.get_by_id_with_object(status_id),
|
||||
{:visibility, visibility} when visibility in ["public", "unlisted"] <-
|
||||
{:visibility, Visibility.get_visibility(object)},
|
||||
{:language, language} when is_binary(language) <-
|
||||
{:language, Map.get(params, :lang) || user.language},
|
||||
{:ok, result} <-
|
||||
Translation.translate(
|
||||
object.data["content"],
|
||||
object.data["language"],
|
||||
language
|
||||
) do
|
||||
render(conn, "translation.json", result)
|
||||
else
|
||||
{: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")
|
||||
end
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/favourites"
|
||||
def favourites(
|
||||
%{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn,
|
||||
|
|
|
@ -90,6 +90,15 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
}
|
||||
end
|
||||
|
||||
def render("translation_languages.json", _) do
|
||||
with true <- Pleroma.Language.Translation.configured?(),
|
||||
{:ok, languages} <- Pleroma.Language.Translation.languages_matrix() do
|
||||
languages
|
||||
else
|
||||
_ -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
defp common_information(instance) do
|
||||
%{
|
||||
languages: Keyword.get(instance, :languages, ["en"]),
|
||||
|
@ -247,7 +256,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
},
|
||||
vapid: %{
|
||||
public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
||||
}
|
||||
},
|
||||
translation: %{enabled: Pleroma.Language.Translation.configured?()}
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -260,7 +270,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
fields_limits: fields_limits(),
|
||||
post_formats: Config.get([:instance, :allowed_post_formats]),
|
||||
birthday_required: Config.get([:instance, :birthday_required]),
|
||||
birthday_min_age: Config.get([:instance, :birthday_min_age])
|
||||
birthday_min_age: Config.get([:instance, :birthday_min_age]),
|
||||
translation: supported_languages()
|
||||
},
|
||||
stats: %{mau: Pleroma.User.active_user_count()},
|
||||
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
||||
|
@ -286,4 +297,29 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
})
|
||||
})
|
||||
end
|
||||
|
||||
defp supported_languages do
|
||||
enabled = Pleroma.Language.Translation.configured?()
|
||||
|
||||
source_languages =
|
||||
with true <- enabled,
|
||||
{:ok, languages} <- Pleroma.Language.Translation.supported_languages(:source) do
|
||||
languages
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
target_languages =
|
||||
with true <- enabled,
|
||||
{:ok, languages} <- Pleroma.Language.Translation.supported_languages(:target) do
|
||||
languages
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
%{
|
||||
source_languages: source_languages,
|
||||
target_languages: target_languages
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -684,6 +684,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
}
|
||||
end
|
||||
|
||||
def render("translation.json", %{
|
||||
content: content,
|
||||
detected_source_language: detected_source_language,
|
||||
provider: provider
|
||||
}) do
|
||||
%{content: content, detected_source_language: detected_source_language, provider: provider}
|
||||
end
|
||||
|
||||
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Web.RichMedia.Parser do
|
||||
alias Pleroma.Web.RichMedia.Helpers
|
||||
import Pleroma.Web.Metadata.Utils, only: [scrub_html_and_truncate: 2]
|
||||
require Logger
|
||||
|
||||
@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
|
||||
|
@ -63,8 +64,20 @@ defmodule Pleroma.Web.RichMedia.Parser do
|
|||
not match?({:ok, _}, Jason.encode(%{key => val}))
|
||||
end)
|
||||
|> Map.new()
|
||||
|> truncate_title()
|
||||
|> truncate_desc()
|
||||
end
|
||||
|
||||
defp truncate_title(%{"title" => title} = data) when is_binary(title),
|
||||
do: %{data | "title" => scrub_html_and_truncate(title, 120)}
|
||||
|
||||
defp truncate_title(data), do: data
|
||||
|
||||
defp truncate_desc(%{"description" => desc} = data) when is_binary(desc),
|
||||
do: %{data | "description" => scrub_html_and_truncate(desc, 200)}
|
||||
|
||||
defp truncate_desc(data), do: data
|
||||
|
||||
@spec validate_page_url(URI.t() | binary()) :: :ok | :error
|
||||
defp validate_page_url(page_url) when is_binary(page_url) do
|
||||
validate_tld = @config_impl.get([Pleroma.Formatter, :validate_tld])
|
||||
|
|
|
@ -764,6 +764,7 @@ defmodule Pleroma.Web.Router do
|
|||
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
|
||||
post("/statuses/:id/mute", StatusController, :mute_conversation)
|
||||
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
|
||||
post("/statuses/:id/translate", StatusController, :translate)
|
||||
|
||||
post("/push/subscription", SubscriptionController, :create)
|
||||
get("/push/subscription", SubscriptionController, :show)
|
||||
|
@ -811,6 +812,7 @@ defmodule Pleroma.Web.Router do
|
|||
get("/instance", InstanceController, :show)
|
||||
get("/instance/peers", InstanceController, :peers)
|
||||
get("/instance/rules", InstanceController, :rules)
|
||||
get("/instance/translation_languages", InstanceController, :translation_languages)
|
||||
|
||||
get("/statuses", StatusController, :index)
|
||||
get("/statuses/:id", StatusController, :show)
|
||||
|
|
54
test/fixtures/misskey-custom-emoji-like.json
vendored
Normal file
54
test/fixtures/misskey-custom-emoji-like.json
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"Emoji": "toot:Emoji",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"_misskey_content": "misskey:_misskey_content",
|
||||
"_misskey_quote": "misskey:_misskey_quote",
|
||||
"_misskey_reaction": "misskey:_misskey_reaction",
|
||||
"_misskey_summary": "misskey:_misskey_summary",
|
||||
"_misskey_votes": "misskey:_misskey_votes",
|
||||
"backgroundUrl": "sharkey:backgroundUrl",
|
||||
"discoverable": "toot:discoverable",
|
||||
"featured": "toot:featured",
|
||||
"fedibird": "http://fedibird.com/ns#",
|
||||
"firefish": "https://joinfirefish.org/ns#",
|
||||
"isCat": "misskey:isCat",
|
||||
"listenbrainz": "sharkey:listenbrainz",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"misskey": "https://misskey-hub.net/ns#",
|
||||
"quoteUri": "fedibird:quoteUri",
|
||||
"quoteUrl": "as:quoteUrl",
|
||||
"schema": "http://schema.org#",
|
||||
"sensitive": "as:sensitive",
|
||||
"sharkey": "https://joinsharkey.org/ns#",
|
||||
"speakAsCat": "firefish:speakAsCat",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"value": "schema:value",
|
||||
"vcard": "http://www.w3.org/2006/vcard/ns#"
|
||||
}
|
||||
],
|
||||
"_misskey_reaction": ":blobwtfnotlikethis:",
|
||||
"actor": "https://mai.waifuism.life/users/9otxaeemjqy70001",
|
||||
"content": ":blobwtfnotlikethis:",
|
||||
"id": "https://mai.waifuism.life/likes/9q2xifhrdnb0001b",
|
||||
"object": "https://bungle.online/notes/9q2xi2sy4k",
|
||||
"tag": [
|
||||
{
|
||||
"icon": {
|
||||
"mediaType": "image/png",
|
||||
"type": "Image",
|
||||
"url": "https://mai.waifuism.life/files/1b0510f2-1fb4-43f5-a399-10053bbd8f0f"
|
||||
},
|
||||
"id": "https://mai.waifuism.life/emojis/blobwtfnotlikethis",
|
||||
"name": ":blobwtfnotlikethis:",
|
||||
"type": "Emoji",
|
||||
"updated": "2024-02-07T02:21:46.497Z"
|
||||
}
|
||||
],
|
||||
"type": "Like"
|
||||
}
|
||||
|
46
test/fixtures/mitra-custom-emoji-like.json
vendored
Normal file
46
test/fixtures/mitra-custom-emoji-like.json
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
"https://w3id.org/security/data-integrity/v1",
|
||||
{
|
||||
"Emoji": "toot:Emoji",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"sensitive": "as:sensitive",
|
||||
"toot": "http://joinmastodon.org/ns#"
|
||||
}
|
||||
],
|
||||
"actor": "https://mitra.social/users/silverpill",
|
||||
"cc": [],
|
||||
"content": ":ablobcatheartsqueeze:",
|
||||
"id": "https://mitra.social/activities/like/0195a89a-a3a0-ead4-3a1c-aa6311397cfd",
|
||||
"object": "https://framapiaf.org/users/peertube/statuses/114182703352270287",
|
||||
"proof": {
|
||||
"created": "2025-03-18T09:34:21.610678375Z",
|
||||
"cryptosuite": "eddsa-jcs-2022",
|
||||
"proofPurpose": "assertionMethod",
|
||||
"proofValue": "z5AvpwkXQGFpTneRVDNeF48Jo9qYG6PgrE5HaPPpQNdNyc31ULMN4Vxd4aFXELo4Rk5Y9hd9nDy254xP8v5uGGWp1",
|
||||
"type": "DataIntegrityProof",
|
||||
"verificationMethod": "https://mitra.social/users/silverpill#ed25519-key"
|
||||
},
|
||||
"tag": [
|
||||
{
|
||||
"attributedTo": "https://mitra.social/actor",
|
||||
"icon": {
|
||||
"mediaType": "image/png",
|
||||
"type": "Image",
|
||||
"url": "https://mitra.social/media/a08e153441b25e512ab1b2e8922f5d8cd928322c8b79958cd48297ac722a4117.png"
|
||||
},
|
||||
"id": "https://mitra.social/objects/emojis/ablobcatheartsqueeze",
|
||||
"name": ":ablobcatheartsqueeze:",
|
||||
"type": "Emoji",
|
||||
"updated": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"to": [
|
||||
"https://framapiaf.org/users/peertube",
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"type": "Like"
|
||||
}
|
||||
|
90
test/fixtures/rich_media/instagram_longtext.html
vendored
Normal file
90
test/fixtures/rich_media/instagram_longtext.html
vendored
Normal file
File diff suppressed because one or more lines are too long
1
test/fixtures/tesla_mock/deepl-languages-list.json
vendored
Normal file
1
test/fixtures/tesla_mock/deepl-languages-list.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
[{"language":"BG","name":"Bulgarian","supports_formality":false},{"language":"CS","name":"Czech","supports_formality":false},{"language":"DA","name":"Danish","supports_formality":false},{"language":"DE","name":"German","supports_formality":true},{"language":"EL","name":"Greek","supports_formality":false},{"language":"EN-GB","name":"English (British)","supports_formality":false},{"language":"EN-US","name":"English (American)","supports_formality":false},{"language":"ES","name":"Spanish","supports_formality":true},{"language":"ET","name":"Estonian","supports_formality":false},{"language":"FI","name":"Finnish","supports_formality":false},{"language":"FR","name":"French","supports_formality":true},{"language":"HU","name":"Hungarian","supports_formality":false},{"language":"ID","name":"Indonesian","supports_formality":false},{"language":"IT","name":"Italian","supports_formality":true},{"language":"JA","name":"Japanese","supports_formality":false},{"language":"LT","name":"Lithuanian","supports_formality":false},{"language":"LV","name":"Latvian","supports_formality":false},{"language":"NL","name":"Dutch","supports_formality":true},{"language":"PL","name":"Polish","supports_formality":true},{"language":"PT-BR","name":"Portuguese (Brazilian)","supports_formality":true},{"language":"PT-PT","name":"Portuguese (European)","supports_formality":true},{"language":"RO","name":"Romanian","supports_formality":false},{"language":"RU","name":"Russian","supports_formality":true},{"language":"SK","name":"Slovak","supports_formality":false},{"language":"SL","name":"Slovenian","supports_formality":false},{"language":"SV","name":"Swedish","supports_formality":false},{"language":"TR","name":"Turkish","supports_formality":false},{"language":"UK","name":"Ukrainian","supports_formality":false},{"language":"ZH","name":"Chinese (simplified)","supports_formality":false}]
|
1
test/fixtures/tesla_mock/deepl-translation.json
vendored
Normal file
1
test/fixtures/tesla_mock/deepl-translation.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"translations":[{"detected_source_language":"PL","text":"REMOVE THE FOLLOWER!Paste this on your follower. If we get 70% of nk users...they will remove the follower!!!"}]}
|
37
test/pleroma/language/translation/deepl_test.exs
Normal file
37
test/pleroma/language/translation/deepl_test.exs
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.DeeplTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Language.Translation.Deepl
|
||||
|
||||
test "it translates text" do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com")
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY")
|
||||
|
||||
{:ok, res} =
|
||||
Deepl.translate(
|
||||
"USUNĄĆ ŚLEDZIKA!Wklej to na swojego śledzika. Jeżeli uzbieramy 70% użytkowników nk...to usuną śledzika!!!",
|
||||
"pl",
|
||||
"en"
|
||||
)
|
||||
|
||||
assert %{
|
||||
detected_source_language: "PL",
|
||||
provider: "DeepL"
|
||||
} = res
|
||||
end
|
||||
|
||||
test "it returns languages list" do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com")
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY")
|
||||
|
||||
assert {:ok, [language | _languages]} = Deepl.supported_languages(:target)
|
||||
|
||||
assert is_binary(language)
|
||||
end
|
||||
end
|
28
test/pleroma/language/translation_test.exs
Normal file
28
test/pleroma/language/translation_test.exs
Normal file
|
@ -0,0 +1,28 @@
|
|||
defmodule Pleroma.Language.TranslationTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Language.Translation
|
||||
|
||||
setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
|
||||
|
||||
test "it translates text" do
|
||||
assert {:ok,
|
||||
%{
|
||||
content: "txet emos",
|
||||
detected_source_language: _,
|
||||
provider: _
|
||||
}} = Translation.translate("some text", "en", "uk")
|
||||
end
|
||||
|
||||
test "it stores translation result in cache" do
|
||||
Translation.translate("some text", "en", "uk")
|
||||
|
||||
assert {:ok, result} =
|
||||
Cachex.get(
|
||||
:translations_cache,
|
||||
"en/uk/#{:crypto.hash(:sha256, "some text") |> Base.encode64()}"
|
||||
)
|
||||
|
||||
assert result.content == "txet emos"
|
||||
end
|
||||
end
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do
|
|||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
|
@ -75,4 +76,71 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do
|
|||
assert activity_data["object"] == activity.data["object"]
|
||||
assert activity_data["content"] == "⭐"
|
||||
end
|
||||
|
||||
test "it works for misskey likes with custom emoji" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
|
||||
|
||||
data =
|
||||
File.read!("test/fixtures/misskey-custom-emoji-like.json")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("object", activity.data["object"])
|
||||
|
||||
_actor = insert(:user, ap_id: data["actor"], local: false)
|
||||
|
||||
{:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||
|
||||
assert activity_data["actor"] == data["actor"]
|
||||
assert activity_data["type"] == "EmojiReact"
|
||||
assert activity_data["id"] == data["id"]
|
||||
assert activity_data["object"] == activity.data["object"]
|
||||
assert activity_data["content"] == ":blobwtfnotlikethis:"
|
||||
|
||||
assert [["blobwtfnotlikethis", _, _]] =
|
||||
Object.get_by_ap_id(activity.data["object"])
|
||||
|> Object.get_emoji_reactions()
|
||||
end
|
||||
|
||||
test "it works for mitra likes with custom emoji" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
|
||||
|
||||
data =
|
||||
File.read!("test/fixtures/mitra-custom-emoji-like.json")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("object", activity.data["object"])
|
||||
|
||||
_actor = insert(:user, ap_id: data["actor"], local: false)
|
||||
|
||||
{:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||
|
||||
assert activity_data["actor"] == data["actor"]
|
||||
assert activity_data["type"] == "EmojiReact"
|
||||
assert activity_data["id"] == data["id"]
|
||||
assert activity_data["object"] == activity.data["object"]
|
||||
assert activity_data["content"] == ":ablobcatheartsqueeze:"
|
||||
|
||||
assert [["ablobcatheartsqueeze", _, _]] =
|
||||
Object.get_by_ap_id(activity.data["object"])
|
||||
|> Object.get_emoji_reactions()
|
||||
end
|
||||
|
||||
test "it works for likes with wrong content" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
|
||||
|
||||
data =
|
||||
File.read!("test/fixtures/mitra-custom-emoji-like.json")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("object", activity.data["object"])
|
||||
|> Map.put("content", 1)
|
||||
|
||||
_actor = insert(:user, ap_id: data["actor"], local: false)
|
||||
|
||||
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
|
||||
assert activity.data["type"] == "Like"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -152,4 +152,13 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
|
|||
}
|
||||
] = result["rules"]
|
||||
end
|
||||
|
||||
test "translation languages matrix", %{conn: conn} do
|
||||
clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
|
||||
|
||||
assert %{"en" => ["pl"], "pl" => ["en"]} =
|
||||
conn
|
||||
|> get("/api/v1/instance/translation_languages")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2500,4 +2500,62 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
|||
|> json_response_and_validate_schema(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
describe "translating statuses" do
|
||||
setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
|
||||
|
||||
test "it translates a status to user language" do
|
||||
user = insert(:user, language: "fr")
|
||||
%{conn: conn} = oauth_access(["read:statuses"], user: user)
|
||||
another_user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(another_user, %{
|
||||
status: "Cześć!",
|
||||
visibility: "public",
|
||||
language: "pl"
|
||||
})
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/api/v1/statuses/#{activity.id}/translate")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert response == %{
|
||||
"content" => "!ćśezC",
|
||||
"detected_source_language" => "pl",
|
||||
"provider" => "TranslationMock"
|
||||
}
|
||||
end
|
||||
|
||||
test "it returns an error if no target language provided" do
|
||||
%{conn: conn} = oauth_access(["read:statuses"])
|
||||
another_user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(another_user, %{
|
||||
status: "Cześć!",
|
||||
language: "pl"
|
||||
})
|
||||
|
||||
assert conn
|
||||
|> post("/api/v1/statuses/#{activity.id}/translate")
|
||||
|> json_response_and_validate_schema(400)
|
||||
end
|
||||
|
||||
test "it doesn't translate non-public statuses" do
|
||||
%{conn: conn, user: user} = oauth_access(["read:statuses"])
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Cześć!",
|
||||
visibility: "private",
|
||||
language: "pl"
|
||||
})
|
||||
|
||||
assert conn
|
||||
|> post("/api/v1/statuses/#{activity.id}/translate")
|
||||
|> json_response_and_validate_schema(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -61,6 +61,13 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
|
|||
}}
|
||||
end
|
||||
|
||||
test "truncates title and description fields" do
|
||||
{:ok, parsed} = Parser.parse("https://instagram.com/longtext")
|
||||
|
||||
assert String.length(parsed["title"]) == 120
|
||||
assert String.length(parsed["description"]) == 200
|
||||
end
|
||||
|
||||
test "parses OEmbed and filters HTML tags" do
|
||||
assert Parser.parse("https://example.com/oembed") ==
|
||||
{:ok,
|
||||
|
|
|
@ -1494,6 +1494,11 @@ defmodule HttpRequestMock do
|
|||
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}}
|
||||
end
|
||||
|
||||
def get("https://instagram.com/longtext", _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/instagram_longtext.html")}}
|
||||
end
|
||||
|
||||
def get("https://example.com/non-ogp", _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")}}
|
||||
|
@ -1764,6 +1769,24 @@ defmodule HttpRequestMock do
|
|||
}}
|
||||
end
|
||||
|
||||
def post("https://api-free.deepl.com/v2/translate" <> _, _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/deepl-translation.json"),
|
||||
headers: [{"content-type", "application/json"}]
|
||||
}}
|
||||
end
|
||||
|
||||
def post("https://api-free.deepl.com/v2/languages" <> _, _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/deepl-languages-list.json"),
|
||||
headers: [{"content-type", "application/json"}]
|
||||
}}
|
||||
end
|
||||
|
||||
def post(url, query, body, headers) do
|
||||
{:error,
|
||||
"Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}
|
||||
|
@ -1783,7 +1806,8 @@ defmodule HttpRequestMock do
|
|||
"https://example.com/twitter-card",
|
||||
"https://google.com/",
|
||||
"https://pleroma.local/notice/9kCP7V",
|
||||
"https://yahoo.com/"
|
||||
"https://yahoo.com/",
|
||||
"https://instagram.com/longtext"
|
||||
]
|
||||
|
||||
def head(url, _query, _body, _headers) when url in @rich_media_mocks do
|
||||
|
|
43
test/support/translation_mock.ex
Normal file
43
test/support/translation_mock.ex
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule TranslationMock do
|
||||
alias Pleroma.Language.Translation.Provider
|
||||
|
||||
use Provider
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@name "TranslationMock"
|
||||
|
||||
@impl Provider
|
||||
def configured?, do: true
|
||||
|
||||
@impl Provider
|
||||
def translate(content, source_language, _target_language) do
|
||||
{:ok,
|
||||
%{
|
||||
content: content |> String.reverse(),
|
||||
detected_source_language: source_language,
|
||||
provider: @name
|
||||
}}
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def supported_languages(_) do
|
||||
{:ok, ["en", "pl"]}
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def languages_matrix do
|
||||
{:ok,
|
||||
%{
|
||||
"en" => ["pl"],
|
||||
"pl" => ["en"]
|
||||
}}
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def name, do: @name
|
||||
end
|
Loading…
Reference in a new issue