Merge remote-tracking branch 'origin/develop' into shigusegubu

This commit is contained in:
Henry Jameson 2025-06-25 14:23:41 +03:00
commit 63b82091d9
153 changed files with 3143 additions and 539 deletions

View file

@ -271,7 +271,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
[config_dir, psql_dir, static_dir, uploads_dir]
|> Enum.reject(&File.exists?/1)
|> Enum.each(fn dir ->
File.mkdir_p!(dir)
Pleroma.Backports.mkdir_p!(dir)
File.chmod!(dir, 0o700)
end)

View file

@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.RobotsTxt do
static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
if !File.exists?(static_dir) do
File.mkdir_p!(static_dir)
Pleroma.Backports.mkdir_p!(static_dir)
end
robots_txt_path = Path.join(static_dir, "robots.txt")

View file

@ -4,7 +4,9 @@ defmodule Mix.Tasks.Pleroma.TestRunner do
use Mix.Task
def run(args \\ []) do
case System.cmd("mix", ["test"] ++ args, into: IO.stream(:stdio, :line)) do
case System.cmd("mix", ["test", "--warnings-as-errors"] ++ args,
into: IO.stream(:stdio, :line)
) do
{_, 0} ->
:ok

View file

@ -43,9 +43,6 @@ defmodule Pleroma.Application do
# every time the application is restarted, so we disable module
# conflicts at runtime
Code.compiler_options(ignore_module_conflict: true)
# Disable warnings_as_errors at runtime, it breaks Phoenix live reload
# due to protocol consolidation warnings
Code.compiler_options(warnings_as_errors: false)
Pleroma.Telemetry.Logger.attach()
Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers()
@ -56,7 +53,10 @@ defmodule Pleroma.Application do
Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled()
end
Pleroma.ApplicationRequirements.verify!()
if Config.get(:env) != :test do
Pleroma.ApplicationRequirements.verify!()
end
load_custom_modules()
Pleroma.Docs.JSON.compile()
limiters_setup()
@ -68,26 +68,11 @@ defmodule Pleroma.Application do
Finch.start_link(name: MyFinch)
end
if adapter == Tesla.Adapter.Gun do
if version = Pleroma.OTPVersion.version() do
[major, minor] =
version
|> String.split(".")
|> Enum.map(&String.to_integer/1)
|> Enum.take(2)
if (major == 22 and minor < 2) or major < 22 do
raise "
!!!OTP VERSION WARNING!!!
You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2.
"
end
else
raise "
!!!OTP VERSION WARNING!!!
To support correct handling of unordered certificates chains - OTP version must be > 22.2.
"
end
# Disable warnings_as_errors at runtime, it breaks Phoenix live reload
# due to protocol consolidation warnings
# :warnings_as_errors is deprecated via Code.compiler_options/2 since 1.18
if Version.compare(System.version(), "1.18.0") == :lt do
Code.compiler_options(warnings_as_errors: false)
end
# Define workers and child supervisors to be supervised
@ -169,7 +154,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

@ -189,7 +189,40 @@ defmodule Pleroma.ApplicationRequirements do
false
end
if Enum.all?([preview_proxy_commands_status | filter_commands_statuses], & &1) do
language_detector_commands_status =
if Pleroma.Language.LanguageDetector.missing_dependencies() == [] do
true
else
Logger.error(
"The following dependencies required by the currently enabled " <>
"language detection provider are not installed: " <>
inspect(Pleroma.Language.LanguageDetector.missing_dependencies())
)
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,
translation_commands_status | filter_commands_statuses
],
& &1
) do
:ok
else
{:error,

72
lib/pleroma/backports.ex Normal file
View file

@ -0,0 +1,72 @@
# Copyright 2012 Plataformatec
# Copyright 2021 The Elixir Team
# SPDX-License-Identifier: Apache-2.0
defmodule Pleroma.Backports do
import File, only: [dir?: 1]
# <https://github.com/elixir-lang/elixir/pull/14242>
# To be removed when we require Elixir 1.19
@doc """
Tries to create the directory `path`.
Missing parent directories are created. Returns `:ok` if successful, or
`{:error, reason}` if an error occurs.
Typical error reasons are:
* `:eacces` - missing search or write permissions for the parent
directories of `path`
* `:enospc` - there is no space left on the device
* `:enotdir` - a component of `path` is not a directory
"""
@spec mkdir_p(Path.t()) :: :ok | {:error, File.posix() | :badarg}
def mkdir_p(path) do
do_mkdir_p(IO.chardata_to_string(path))
end
defp do_mkdir_p("/") do
:ok
end
defp do_mkdir_p(path) do
parent = Path.dirname(path)
if parent == path do
:ok
else
case do_mkdir_p(parent) do
:ok ->
case :file.make_dir(path) do
{:error, :eexist} ->
if dir?(path), do: :ok, else: {:error, :enotdir}
other ->
other
end
e ->
e
end
end
end
@doc """
Same as `mkdir_p/1`, but raises a `File.Error` exception in case of failure.
Otherwise `:ok`.
"""
@spec mkdir_p!(Path.t()) :: :ok
def mkdir_p!(path) do
case mkdir_p(path) do
:ok ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
action: "make directory (with -p)",
path: IO.chardata_to_string(path)
end
end
end

View file

@ -302,7 +302,7 @@ defmodule Pleroma.ConfigDB do
end
def to_elixir_types(%{"tuple" => entity}) do
Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1)))
Enum.reduce(entity, {}, &Tuple.insert_at(&2, tuple_size(&2), to_elixir_types(&1)))
end
def to_elixir_types(entity) when is_map(entity) do

View file

@ -100,6 +100,7 @@ defmodule Pleroma.Constants do
"Add",
"Remove",
"Like",
"Dislike",
"Announce",
"Undo",
"Flag",
@ -115,6 +116,7 @@ defmodule Pleroma.Constants do
"Flag",
"Follow",
"Like",
"Dislike",
"EmojiReact",
"Announce"
]

View file

@ -488,7 +488,7 @@ defmodule Pleroma.Emoji.Pack do
with true <- String.contains?(file_path, "/"),
path <- Path.dirname(file_path),
false <- File.exists?(path) do
File.mkdir_p!(path)
Pleroma.Backports.mkdir_p!(path)
end
end
@ -536,7 +536,7 @@ defmodule Pleroma.Emoji.Pack do
emoji_path = emoji_path()
# Create the directory first if it does not exist. This is probably the first request made
# with the API so it should be sufficient
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
with {:create_dir, :ok} <- {:create_dir, Pleroma.Backports.mkdir_p(emoji_path)},
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
{:ok, Enum.sort(results)}
else
@ -561,7 +561,7 @@ defmodule Pleroma.Emoji.Pack do
end
defp unzip(archive, pack_info, remote_pack, local_pack) do
with :ok <- File.mkdir_p!(local_pack.path) do
with :ok <- Pleroma.Backports.mkdir_p!(local_pack.path) do
files = Enum.map(remote_pack["files"], fn {_, path} -> path end)
# Fallback cannot contain a pack.json file
files = if pack_info[:fallback], do: files, else: ["pack.json" | files]

View file

@ -66,7 +66,7 @@ defmodule Pleroma.Frontend do
def unzip(zip, dest) do
File.rm_rf!(dest)
File.mkdir_p!(dest)
Pleroma.Backports.mkdir_p!(dest)
case Pleroma.SafeZip.unzip_data(zip, dest) do
{:ok, _} -> :ok
@ -90,7 +90,7 @@ defmodule Pleroma.Frontend do
defp install_frontend(frontend_info, source, dest) do
from = frontend_info["build_dir"] || "dist"
File.rm_rf!(dest)
File.mkdir_p!(dest)
Pleroma.Backports.mkdir_p!(dest)
File.cp_r!(Path.join([source, from]), dest)
:ok
end

View file

@ -0,0 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Language.LanguageDetector do
import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode,
only: [good_locale_code?: 1]
@words_threshold 4
@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
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
# Strip tags from text, etc.
defp prepare_text(text) do
text
|> Floki.parse_fragment!()
|> Floki.filter_out(
".h-card, .mention, .hashtag, .u-url, .quote-inline, .recipients-inline, code, pre"
)
|> Floki.text()
end
def detect(text) do
provider = get_provider()
text = prepare_text(text)
word_count = text |> String.split(~r/\s+/) |> Enum.count()
if word_count < @words_threshold or !provider or !provider.configured?() do
nil
else
with language <- provider.detect(text),
true <- good_locale_code?(language) do
language
else
_ -> nil
end
end
end
defp get_provider do
@config_impl.get([__MODULE__, :provider])
end
end

View file

@ -0,0 +1,47 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Language.LanguageDetector.Fasttext do
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
alias Pleroma.Language.LanguageDetector.Provider
@behaviour Provider
@impl Provider
def missing_dependencies do
if Pleroma.Utils.command_available?("fasttext") do
[]
else
["fasttext"]
end
end
@impl Provider
def configured?, do: not_empty_string(get_model())
@impl Provider
def detect(text) do
text_path = Path.join(System.tmp_dir!(), "fasttext-#{Ecto.UUID.generate()}")
File.write(text_path, text |> String.replace(~r/\s+/, " "))
detected_language =
case System.cmd("fasttext", ["predict", get_model(), text_path]) do
{"__label__" <> language, _} ->
language |> String.trim()
_ ->
nil
end
File.rm(text_path)
detected_language
end
defp get_model do
Pleroma.Config.get([__MODULE__, :model])
end
end

View file

@ -0,0 +1,11 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Language.LanguageDetector.Provider do
@callback missing_dependencies() :: [String.t()]
@callback configured?() :: boolean()
@callback detect(text :: String.t()) :: String.t() | nil
end

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,121 @@
# 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,
Jason.encode!(%{
text: [content],
source_lang: source_language |> String.upcase(),
target_lang: target_language,
tag_handling: "html"
}),
[
{"Content-Type", "application/json"},
{"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

@ -1,28 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.OTPVersion do
@spec version() :: String.t() | nil
def version do
# OTP Version https://erlang.org/doc/system_principles/versions.html#otp-version
[
Path.join(:code.root_dir(), "OTP_VERSION"),
Path.join([:code.root_dir(), "releases", :erlang.system_info(:otp_release), "OTP_VERSION"])
]
|> get_version_from_files()
end
@spec get_version_from_files([Path.t()]) :: String.t() | nil
def get_version_from_files([]), do: nil
def get_version_from_files([path | paths]) do
if File.exists?(path) do
path
|> File.read!()
|> String.replace(~r/\r|\n|\s/, "")
else
get_version_from_files(paths)
end
end
end

View file

@ -102,7 +102,8 @@ defmodule Pleroma.Search.DatabaseSearch do
^tsc,
o.data,
^search_query
)
),
order_by: [desc: :inserted_at]
)
end

View file

@ -19,7 +19,7 @@ defmodule Pleroma.Uploaders.Local do
[file | folders] ->
path = Path.join([upload_path()] ++ Enum.reverse(folders))
File.mkdir_p!(path)
Pleroma.Backports.mkdir_p!(path)
{path, file}
end

View file

@ -150,7 +150,7 @@ defmodule Pleroma.User do
field(:allow_following_move, :boolean, default: true)
field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
field(:also_known_as, {:array, ObjectValidators.BareUri}, default: [])
field(:inbox, :string)
field(:shared_inbox, :string)
field(:accepts_chat_messages, :boolean, default: nil)
@ -308,7 +308,7 @@ defmodule Pleroma.User do
def binary_id(%User{} = user), do: binary_id(user.id)
@doc "Returns status account"
@doc "Returns account status"
@spec account_status(User.t()) :: account_status()
def account_status(%User{is_active: false}), do: :deactivated
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
@ -895,7 +895,7 @@ defmodule Pleroma.User do
end)
end
def validate_email_not_in_blacklisted_domain(changeset, field) do
defp validate_email_not_in_blacklisted_domain(changeset, field) do
validate_change(changeset, field, fn _, value ->
valid? =
Config.get([User, :email_blacklist])
@ -912,9 +912,9 @@ defmodule Pleroma.User do
end)
end
def maybe_validate_required_email(changeset, true), do: changeset
defp maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _) do
defp maybe_validate_required_email(changeset, _) do
if Config.get([:instance, :account_activation_required]) do
validate_required(changeset, [:email])
else
@ -1109,15 +1109,15 @@ defmodule Pleroma.User do
defp maybe_send_registration_email(_), do: {:ok, :noop}
def needs_update?(%User{local: true}), do: false
defp needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
defp needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
def needs_update?(%User{local: false} = user) do
defp needs_update?(%User{local: false} = user) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
end
def needs_update?(_), do: true
defp needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) ::
{:ok, User.t(), User.t()} | {:error, String.t()}
@ -1708,7 +1708,9 @@ defmodule Pleroma.User do
end
end
def block(%User{} = blocker, %User{} = blocked) do
def block(blocker, blocked, params \\ %{})
def block(%User{} = blocker, %User{} = blocked, params) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
if following?(blocker, blocked) do
@ -1738,12 +1740,33 @@ defmodule Pleroma.User do
{:ok, blocker} = update_follower_count(blocker)
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
add_to_block(blocker, blocked)
duration = Map.get(params, :duration, 0)
expires_at =
if duration > 0 do
DateTime.utc_now()
|> DateTime.add(duration)
else
nil
end
user_block = add_to_block(blocker, blocked, expires_at)
if duration > 0 do
Pleroma.Workers.MuteExpireWorker.new(
%{"op" => "unblock_user", "blocker_id" => blocker.id, "blocked_id" => blocked.id},
scheduled_at: expires_at
)
|> Oban.insert()
end
user_block
end
# helper to handle the block given only an actor's AP id
def block(%User{} = blocker, %{ap_id: ap_id}) do
block(blocker, get_cached_by_ap_id(ap_id))
def block(%User{} = blocker, %{ap_id: ap_id}, params) do
block(blocker, get_cached_by_ap_id(ap_id), params)
end
def unblock(%User{} = blocker, %User{} = blocked) do
@ -1984,7 +2007,7 @@ defmodule Pleroma.User do
end
@spec purge_user_changeset(User.t()) :: Ecto.Changeset.t()
def purge_user_changeset(user) do
defp purge_user_changeset(user) do
# "Right to be forgotten"
# https://gdpr.eu/right-to-be-forgotten/
change(user, %{
@ -2156,7 +2179,7 @@ defmodule Pleroma.User do
Repo.all(query)
end
def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
defp delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
Notification
|> join(:inner, [n], activity in assoc(n, :activity))
|> where([n, a], fragment("? = ?", a.actor, ^ap_id))
@ -2615,7 +2638,7 @@ defmodule Pleroma.User do
end
end
# Internal function; public one is `deactivate/2`
# Internal function; public one is `set_activation/2`
defp set_activation_status(user, status) do
user
|> cast(%{is_active: status}, [:is_active])
@ -2634,7 +2657,7 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
def validate_fields(changeset, remote? \\ false) do
defp validate_fields(changeset, remote?) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Config.get([:instance, limit_name], 0)
@ -2779,10 +2802,10 @@ defmodule Pleroma.User do
set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
end
@spec add_to_block(User.t(), User.t()) ::
@spec add_to_block(User.t(), User.t(), integer() | nil) ::
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
defp add_to_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
defp add_to_block(%User{} = user, %User{} = blocked, expires_at) do
with {:ok, relationship} <- UserRelationship.create_block(user, blocked, expires_at) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
{:ok, relationship}
end

View file

@ -193,7 +193,7 @@ defmodule Pleroma.User.Backup do
backup = Repo.preload(backup, :user)
tempfile = Path.join([backup.tempdir, backup.file_name])
with {_, :ok} <- {:mkdir, File.mkdir_p(backup.tempdir)},
with {_, :ok} <- {:mkdir, Pleroma.Backports.mkdir_p(backup.tempdir)},
{_, :ok} <- {:actor, actor(backup.tempdir, backup.user)},
{_, :ok} <- {:statuses, statuses(backup.tempdir, backup.user)},
{_, :ok} <- {:likes, likes(backup.tempdir, backup.user)},

View file

@ -327,8 +327,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
}, []}
end
@spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
def block(blocker, blocked) do
@spec block(User.t(), User.t(), map()) :: {:ok, map(), keyword()}
def block(blocker, blocked, params \\ %{}) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
@ -336,7 +336,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
"actor" => blocker.ap_id,
"object" => blocked.ap_id,
"to" => [blocked.ap_id]
}, []}
}, Keyword.new(params)}
end
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}

View file

@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
)
File.mkdir_p(emoji_dir_path)
Pleroma.Backports.mkdir_p(emoji_dir_path)
new_emojis =
foreign_emojis

View file

@ -200,14 +200,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
when type in ~w[Accept Reject Follow Like EmojiReact Announce
ChatMessage Answer] do
validator =
case type do
"Accept" -> AcceptRejectValidator
"Reject" -> AcceptRejectValidator
"Follow" -> FollowValidator
"Update" -> UpdateValidator
"Like" -> LikeValidator
"EmojiReact" -> EmojiReactValidator
"Announce" -> AnnounceValidator
@ -215,16 +214,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
"Answer" -> AnswerValidator
end
cast_func =
if type == "Update" do
fn o -> validator.cast_and_validate(o, meta) end
else
fn o -> validator.cast_and_validate(o) end
end
with {:ok, object} <-
object
|> cast_func.()
|> validator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end
def validate(%{"type" => type} = object, meta) when type == "Update" do
with {:ok, object} <-
object
|> UpdateValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Language.LanguageDetector
alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Object.Containment
@ -151,10 +152,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
def maybe_add_language(object) do
language =
[
get_language_from_context(object),
get_language_from_content_map(object)
&get_language_from_context/1,
&get_language_from_content_map/1,
&get_language_from_content/1
]
|> Enum.find(&good_locale_code?(&1))
|> Enum.find_value(fn get_language ->
language = get_language.(object)
if good_locale_code?(language) do
language
else
nil
end
end)
if language do
Map.put(object, "language", language)
@ -187,6 +197,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
defp get_language_from_content_map(_), do: nil
defp get_language_from_content(%{"content" => content} = object) do
LanguageDetector.detect("#{object["summary"] || ""} #{content}")
end
defp get_language_from_content(_), do: nil
def maybe_add_content_map(%{"language" => language, "content" => content} = object)
when not_empty_string(language) do
Map.put(object, "contentMap", Map.put(%{}, language, content))

View file

@ -50,13 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
end
def changeset(struct, %{"type" => "Hashtag", "name" => name} = data) do
name =
cond do
"#" <> name -> name
name -> name
end
|> String.downcase()
name = String.downcase(name)
data = Map.put(data, "name", name)
struct

View file

@ -93,7 +93,20 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
cc = Map.get(params, :cc, [])
param_cc = Map.get(params, :cc, [])
original_cc = Map.get(data, "cc", [])
public_address = Pleroma.Constants.as_public()
# Ensure unlisted posts don't lose the public address in the cc
# if the param_cc was set
cc =
if public_address in original_cc and public_address not in param_cc do
[public_address | param_cc]
else
param_cc
end
json =
data

View file

@ -145,7 +145,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
) do
with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user),
%User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do
User.block(blocker, blocked)
User.block(blocker, blocked, Enum.into(meta, %{}))
end
{:ok, object, meta}

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
@ -652,6 +664,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
# Rewrite dislikes into the thumbs down emoji
defp handle_incoming_normalized(%{"type" => "Dislike"} = data, options) do
data
|> Map.put("type", "EmojiReact")
|> Map.put("content", "👎")
|> handle_incoming_normalized(options)
end
defp handle_incoming_normalized(
%{"type" => "Undo", "object" => %{"type" => "Dislike"}} = data,
options
) do
data
|> put_in(["object", "type"], "EmojiReact")
|> put_in(["object", "content"], "👎")
|> handle_incoming_normalized(options)
end
defp handle_incoming_normalized(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil

View file

@ -335,13 +335,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
if params["password"] do
User.force_password_reset_async(user)
end
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "force_password_reset"
})
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "force_password_reset"
})
end
json(conn, %{status: "success"})
else

View file

@ -97,7 +97,7 @@ defmodule Pleroma.Web.ApiSpec do
"Frontend management",
"Instance configuration",
"Instance documents",
"Instance rule managment",
"Instance rule management",
"Invites",
"MediaProxy cache",
"OAuth application management",

View file

@ -284,18 +284,6 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
:query,
%Schema{allOf: [BooleanLike], default: true},
"Mute notifications in addition to statuses? Defaults to `true`."
),
Operation.parameter(
:duration,
:query,
%Schema{type: :integer},
"Expire the mute in `duration` seconds. Default 0 for infinity"
),
Operation.parameter(
:expires_in,
:query,
%Schema{type: :integer, default: 0},
"Deprecated, use `duration` instead"
)
],
responses: %{
@ -323,16 +311,37 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
tags: ["Account actions"],
summary: "Block",
operationId: "AccountController.block",
requestBody: request_body("Parameters", block_request()),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
description:
"Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end
defp block_request do
%Schema{
title: "AccountBlockRequest",
description: "POST body for blocking an account",
type: :object,
properties: %{
duration: %Schema{
type: :integer,
nullable: true,
description: "Expire the mute in `duration` seconds. Default 0 for infinity"
}
},
example: %{
"duration" => 86_400
}
}
end
def unblock_operation do
%Operation{
tags: ["Account actions"],

View file

@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
def index_operation do
%Operation{
tags: ["Instance rule managment"],
tags: ["Instance rule management"],
summary: "Retrieve list of instance rules",
operationId: "AdminAPI.RuleController.index",
security: [%{"oAuth" => ["admin:read"]}],
@ -33,7 +33,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
def create_operation do
%Operation{
tags: ["Instance rule managment"],
tags: ["Instance rule management"],
summary: "Create new rule",
operationId: "AdminAPI.RuleController.create",
security: [%{"oAuth" => ["admin:write"]}],
@ -49,7 +49,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
def update_operation do
%Operation{
tags: ["Instance rule managment"],
tags: ["Instance rule management"],
summary: "Modify existing rule",
operationId: "AdminAPI.RuleController.update",
security: [%{"oAuth" => ["admin:write"]}],
@ -65,7 +65,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
def delete_operation do
%Operation{
tags: ["Instance rule managment"],
tags: ["Instance rule management"],
summary: "Delete rule",
operationId: "AdminAPI.RuleController.delete",
parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],

View file

@ -52,7 +52,30 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
summary: "Retrieve list of instance rules",
operationId: "InstanceController.rules",
responses: %{
200 => Operation.response("Array of domains", "application/json", array_of_rules())
200 => Operation.response("Array of rules", "application/json", array_of_rules())
}
}
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

View file

@ -59,11 +59,15 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
album: %Schema{type: :string, description: "The album of the media playing"},
artist: %Schema{type: :string, description: "The artist of the media playing"},
length: %Schema{type: :integer, description: "The length of the media playing"},
externalLink: %Schema{type: :string, description: "A URL referencing the media playing"},
external_link: %Schema{type: :string, description: "A URL referencing the media playing"},
visibility: %Schema{
allOf: [VisibilityScope],
default: "public",
description: "Scrobble visibility"
},
externalLink: %Schema{
type: :string,
description: "Deprecated, use `external_link` instead"
}
},
example: %{
@ -71,7 +75,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
"artist" => "Some Artist",
"album" => "Some Album",
"length" => 180_000,
"externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title"
"external_link" => "https://www.last.fm/music/Some+Artist/_/Some+Title"
}
}
end
@ -85,7 +89,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
title: %Schema{type: :string, description: "The title of the media playing"},
album: %Schema{type: :string, description: "The album of the media playing"},
artist: %Schema{type: :string, description: "The artist of the media playing"},
externalLink: %Schema{type: :string, description: "A URL referencing the media playing"},
external_link: %Schema{type: :string, description: "A URL referencing the media playing"},
length: %Schema{
type: :integer,
description: "The length of the media playing",
@ -100,7 +104,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
"artist" => "Some Artist",
"album" => "Some Album",
"length" => 180_000,
"externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title",
"external_link" => "https://www.last.fm/music/Some+Artist/_/Some+Title",
"created_at" => "2019-09-28T12:40:45.000Z"
}
}

View file

@ -427,6 +427,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"],
@ -819,4 +851,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

@ -34,6 +34,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
id: FlakeID,
locked: %Schema{type: :boolean},
mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
block_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
note: %Schema{type: :string, format: :html},
statuses_count: %Schema{type: :integer},
url: %Schema{type: :string, format: :uri},

View file

@ -27,9 +27,9 @@ defmodule Pleroma.Web.CommonAPI do
require Logger
@spec block(User.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def block(blocked, blocker) do
with {:ok, block_data, _} <- Builder.block(blocker, blocked),
{:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
def block(blocked, blocker, params \\ %{}) do
with {:ok, block_data, meta} <- Builder.block(blocker, blocked, params),
{:ok, block, _} <- Pipeline.common_pipeline(block_data, meta ++ [local: true]) do
{:ok, block}
end
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.CommonAPI.ActivityDraft do
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Language.LanguageDetector
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Visibility
@ -90,7 +91,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
defp listen_object(draft) do
object =
draft.params
|> Map.take([:album, :artist, :title, :length, :externalLink])
|> Map.take([:album, :artist, :title, :length])
|> Map.put(:externalLink, Map.get(draft.params, :external_link))
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Audio")
|> Map.put("to", draft.to)
@ -255,13 +257,15 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp language(draft) do
language = draft.params[:language]
language =
with language <- draft.params[:language],
true <- good_locale_code?(language) do
language
else
_ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary)
end
if good_locale_code?(language) do
%__MODULE__{draft | language: language}
else
draft
end
%__MODULE__{draft | language: language}
end
defp object(draft) do

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

@ -46,7 +46,7 @@ defmodule Pleroma.Web.InstanceDocument do
defp put_file(origin_path, destination_path) do
with destination <- instance_static_dir(destination_path),
{_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))},
{_, :ok} <- {:mkdir_p, Pleroma.Backports.mkdir_p(Path.dirname(destination))},
{_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do
:ok
else

View file

@ -501,8 +501,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, _activity} <- CommonAPI.block(blocked, blocker) do
def block(
%{
assigns: %{user: blocker, account: blocked},
private: %{open_api_spex: %{body_params: params}}
} = conn,
_params
) do
with {:ok, _activity} <- CommonAPI.block(blocked, blocker, params) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@ -607,7 +613,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
users: users,
for: user,
as: :user,
embed_relationships: embed_relationships?(params)
embed_relationships: embed_relationships?(params),
blocks: 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

@ -190,7 +190,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
f.()
rescue
error ->
Logger.error("#{__MODULE__} search error: #{inspect(error)}")
Logger.error(Exception.format(:error, error, __STACKTRACE__))
fallback
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
@ -44,6 +45,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
]
)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :translate)
plug(
OAuthScopesPlug,
%{scopes: ["write:statuses"]}
@ -85,7 +88,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,
@ -549,6 +552,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

@ -340,6 +340,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_unread_notification_count(user, opts[:for])
|> maybe_put_email_address(user, opts[:for])
|> maybe_put_mute_expires_at(user, opts[:for], opts)
|> maybe_put_block_expires_at(user, opts[:for], opts)
|> maybe_show_birthday(user, opts[:for])
end
@ -476,6 +477,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_mute_expires_at(data, _, _, _), do: data
defp maybe_put_block_expires_at(data, %User{} = user, target, %{blocks: true}) do
Map.put(
data,
:block_expires_at,
UserRelationship.get_block_expire_date(target, user)
)
end
defp maybe_put_block_expires_at(data, _, _, _), do: data
defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in([:pleroma, :birthday], user.birthday)

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"]),
@ -145,7 +154,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
end,
"pleroma:get:main/ostatus",
"pleroma:group_actors",
"pleroma:bookmark_folders"
"pleroma:bookmark_folders",
if Pleroma.Language.LanguageDetector.configured?() do
"pleroma:language_detection"
end,
"pleroma:block_expiration"
]
|> Enum.filter(& &1)
end
@ -243,11 +256,27 @@ 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
defp pleroma_configuration(instance) do
base_urls = %{}
base_urls =
if Config.get([:media_proxy, :enabled]) do
Map.put(base_urls, :media_proxy, Config.get([:media_proxy, :base_url]))
else
base_urls
end
base_urls =
case Config.get([Pleroma.Upload, :base_url]) do
nil -> base_urls
url -> Map.put(base_urls, :upload, url)
end
%{
metadata: %{
account_activation_required: Keyword.get(instance, :account_activation_required),
@ -256,7 +285,10 @@ 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(),
base_urls: base_urls,
markup: markup()
},
stats: %{mau: Pleroma.User.active_user_count()},
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
@ -282,4 +314,37 @@ 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
defp markup do
%{
allow_inline_images: Config.get([:markup, :allow_inline_images]),
allow_headings: Config.get([:markup, :allow_headings]),
allow_tables: Config.get([:markup, :allow_tables])
}
end
end

View file

@ -681,6 +681,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

@ -24,6 +24,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
params =
params
|> Map.put_new(:external_link, Map.get(params, :externalLink))
with {:ok, activity} <- CommonAPI.listen(user, params) do
render(conn, "show.json", activity: activity, for: user)
else

View file

@ -27,8 +27,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
title: object.data["title"] |> HTML.strip_tags(),
artist: object.data["artist"] |> HTML.strip_tags(),
album: object.data["album"] |> HTML.strip_tags(),
externalLink: object.data["externalLink"],
length: object.data["length"]
external_link: object.data["externalLink"],
length: object.data["length"],
# DEPRECATED
externalLink: object.data["externalLink"]
}
end

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

@ -189,7 +189,7 @@ defmodule Pleroma.Web.Router do
end
pipeline :well_known do
plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml"])
plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml", "html"])
end
pipeline :config do
@ -740,6 +740,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)
@ -787,6 +788,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

@ -231,8 +231,8 @@
</div>
<%= for %{data: mention, object: object, from: from} <- @mentions do %>
<%# mention START %>
<%# user card START %>
<% # mention START %>
<% # user card START %>
<div style="background-color:transparent;">
<div class="block-grid mixed-two-up no-stack"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
@ -291,7 +291,7 @@
</div>
</div>
</div>
<%# user card END %>
<% # user card END %>
<div style="background-color:transparent;">
<div class="block-grid"
@ -333,12 +333,12 @@
</div>
</div>
</div>
<%# mention END %>
<% # mention END %>
<% end %>
<%= if @followers != [] do %>
<%# new followers header START %>
<% # new followers header START %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
@ -397,10 +397,10 @@
</div>
</div>
</div>
<%# new followers header END %>
<% # new followers header END %>
<%= for %{data: follow, from: from} <- @followers do %>
<%# user card START %>
<% # user card START %>
<div style="background-color:transparent;">
<div class="block-grid mixed-two-up no-stack"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
@ -459,13 +459,13 @@
</div>
</div>
</div>
<%# user card END %>
<% # user card END %>
<% end %>
<% end %>
<%# divider start %>
<% # divider start %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
@ -514,7 +514,7 @@
</div>
</div>
<%# divider end %>
<% # divider end %>
<div style="background-color:transparent;">

View file

@ -1,5 +1,5 @@
<%= for {user, total_statuses, latest_status} <- @users_and_statuses do %>
<%# user card START %>
<% # user card START %>
<div style="background-color:transparent;">
<div class="block-grid mixed-two-up no-stack"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
@ -60,7 +60,7 @@
</div>
</div>
</div>
<%# user card END %>
<% # user card END %>
<%= if latest_status do %>
<div style="background-color:transparent;">
@ -104,7 +104,7 @@
</div>
</div>
<% end %>
<%# divider start %>
<% # divider start %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
@ -153,6 +153,6 @@
</div>
</div>
<%# divider end %>
<%# user card END %>
<% # divider end %>
<% # user card END %>
<% end %>

View file

@ -111,7 +111,7 @@
<td style="word-break: break-word; vertical-align: top;" valign="top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color:<%= @styling.background_color %>"><![endif]-->
<%# header %>
<% # header %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
@ -145,7 +145,7 @@
</div>
<%# title %>
<% # title %>
<%= if @title do %>
<div style="background-color:transparent;">
<div class="block-grid"

View file

@ -2,7 +2,7 @@
<%= label @form, :scope, Gettext.dpgettext("static_pages", "oauth scopes message", "The following permissions will be granted") %>
<div class="scopes">
<%= for scope <- @available_scopes do %>
<%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
<% # Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
<%= if scope in @scopes do %>
<div class="scope">
<%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %>

View file

@ -41,5 +41,10 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
end
end
# Default to JSON when no format is specified or format is not recognized
def webfinger(%{assigns: %{format: _format}} = conn, %{"resource" => _resource} = params) do
webfinger(put_in(conn.assigns.format, "json"), params)
end
def webfinger(conn, _params), do: send_resp(conn, 400, "Bad Request")
end

View file

@ -5,9 +5,13 @@
defmodule Pleroma.Workers.MuteExpireWorker do
use Oban.Worker, queue: :background
alias Pleroma.User
@impl true
def perform(%Job{args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}}) do
Pleroma.User.unmute(muter_id, mutee_id)
def perform(%Job{
args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}
}) do
User.unmute(muter_id, mutee_id)
:ok
end
@ -18,6 +22,17 @@ defmodule Pleroma.Workers.MuteExpireWorker do
:ok
end
def perform(%Job{
args: %{"op" => "unblock_user", "blocker_id" => blocker_id, "blocked_id" => blocked_id}
}) do
Pleroma.Web.CommonAPI.unblock(
User.get_cached_by_id(blocked_id),
User.get_cached_by_id(blocker_id)
)
:ok
end
@impl true
def timeout(_job), do: :timer.seconds(5)
end