1
0
Fork 0
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:
mkljczk 2025-03-23 15:05:08 +01:00
commit 26dcb4f964
34 changed files with 1121 additions and 9 deletions

View file

@ -0,0 +1 @@
Support Mitra-style emoji likes.

1
changelog.d/releases.fix Normal file
View file

@ -0,0 +1 @@
Fix release builds

View file

@ -0,0 +1 @@
Support translation providers (DeepL, LibreTranslate)

View file

@ -0,0 +1 @@
Truncate the length of Rich Media title and description fields

View file

@ -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,

View file

@ -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

View file

@ -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

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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])

View file

@ -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)

View 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"
}

View 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"
}

File diff suppressed because one or more lines are too long

View 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}]

View 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!!!"}]}

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View 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