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

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-02-22 11:59:15 +01:00
commit c592a0e58d
184 changed files with 1738 additions and 1213 deletions

6
.dialyzer_ignore.exs Normal file
View File

@ -0,0 +1,6 @@
[
{"lib/cachex.ex", "Unknown type: Spec.cache/0."},
{"lib/pleroma/web/plugs/rate_limiter.ex", "The pattern can never match the type {:commit, _} | {:ignore, _}."},
{"lib/pleroma/web/plugs/rate_limiter.ex", "Function get_scale/2 will never be called."},
{"lib/pleroma/web/plugs/rate_limiter.ex", "Function initialize_buckets!/1 will never be called."}
]

5
.gitignore vendored
View File

@ -57,5 +57,6 @@ pleroma.iml
.tool-versions .tool-versions
# Editor temp files # Editor temp files
/*~ *~
/*# *#
*.swp

View File

@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.6.2
### Security
- MRF StealEmojiPolicy: Sanitize shortcodes (thanks to Hazel K for the report
## 2.6.1 ## 2.6.1
### Changed ### Changed
- - Document maximum supported version of Erlang & Elixir - - Document maximum supported version of Erlang & Elixir

View File

View File

@ -0,0 +1 @@
Support Bandit as an alternative to Cowboy for the HTTP server.

View File

@ -0,0 +1 @@
Fix federation with Convergence AP Bridge

View File

@ -0,0 +1 @@
- Config: Check the permissions of the linked file instead of the symlink

View File

@ -0,0 +1 @@
MediaProxy was setting the content-length header which is not permitted by RFC9112§6.2 when we are chunking the reply as it conflicts with the existence of the transfer-encoding header.

View File

View File

View File

View File

View File

View File

0
changelog.d/exile.skip Normal file
View File

View File

1
changelog.d/gun_pool.fix Normal file
View File

@ -0,0 +1 @@
Fix logic error in Gun connection pooling which prevented retries even when the worker was launched with retry = true

View File

@ -0,0 +1 @@
Mastodon API /api/v1/directory: Fix listing directory contents when not authenticated

1
changelog.d/memleak.fix Normal file
View File

@ -0,0 +1 @@
Fix a memory leak caused by Websocket connections that would not enter a state where a full garbage collection run could be triggered.

View File

View File

@ -0,0 +1 @@
Federated timeline removal of hashtags via MRF HashtagPolicy

View File

@ -0,0 +1 @@
Fix notifications query which was not using the index properly

View File

@ -0,0 +1 @@
Use User.full_nickname/1 in oauth html template

View File

@ -0,0 +1 @@
Rich Media Preview cache eviction when the activity is updated.

View File

1
changelog.d/tesla.deps Normal file
View File

@ -0,0 +1 @@
Update Tesla HTTP client middleware to 1.8.0

View File

@ -0,0 +1 @@
Refactor the Mastodon /api/v1/streaming websocket handler to use Phoenix.Socket.Transport

View File

@ -1,4 +1,4 @@
FROM elixir:1.12.3 FROM elixir:1.15.7-otp-25
# Single RUN statement, otherwise intermediate images are created # Single RUN statement, otherwise intermediate images are created
# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run # https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run

View File

@ -114,14 +114,7 @@
config :pleroma, Pleroma.Web.Endpoint, config :pleroma, Pleroma.Web.Endpoint,
url: [host: "localhost"], url: [host: "localhost"],
http: [ http: [
ip: {127, 0, 0, 1}, ip: {127, 0, 0, 1}
dispatch: [
{:_,
[
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{:_, Plug.Cowboy.Handler, {Pleroma.Web.Endpoint, []}}
]}
]
], ],
protocol: "https", protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl", secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
@ -911,6 +904,8 @@
max_restarts: 3, max_restarts: 3,
streamer_registry: true streamer_registry: true
config :pleroma, Pleroma.Uploaders.Uploader, timeout: 30_000
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View File

@ -8,8 +8,7 @@
# with brunch.io to recompile .js and .css sources. # with brunch.io to recompile .js and .css sources.
config :pleroma, Pleroma.Web.Endpoint, config :pleroma, Pleroma.Web.Endpoint,
http: [ http: [
port: 4000, port: 4000
protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]
], ],
protocol: "http", protocol: "http",
debug_errors: true, debug_errors: true,

View File

@ -170,6 +170,10 @@
streamer_registry: false, streamer_registry: false,
test_http_pools: true test_http_pools: true
config :pleroma, Pleroma.Uploaders.Uploader, timeout: 1_000
config :pleroma, Pleroma.Emoji.Loader, test_emoji: true
if File.exists?("./config/test.secret.exs") do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
else else

View File

@ -111,7 +111,7 @@ def run(["get-packs" | args]) do
{:ok, _} = {:ok, _} =
:zip.unzip(binary_archive, :zip.unzip(binary_archive,
cwd: pack_path, cwd: String.to_charlist(pack_path),
file_list: files_to_unzip file_list: files_to_unzip
) )

View File

@ -1,93 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Phoenix.Transports.WebSocket.Raw do
import Plug.Conn,
only: [
fetch_query_params: 1,
send_resp: 3
]
alias Phoenix.Socket.Transport
def default_config do
[
timeout: 60_000,
transport_log: false,
cowboy: Phoenix.Endpoint.CowboyWebSocket
]
end
def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do
{_, opts} = handler.__transport__(transport)
conn =
conn
|> fetch_query_params
|> Transport.transport_log(opts[:transport_log])
|> Transport.check_origin(handler, endpoint, opts)
case conn do
%{halted: false} = conn ->
case handler.connect(%{
endpoint: endpoint,
transport: transport,
options: [serializer: nil],
params: conn.params
}) do
{:ok, socket} ->
{:ok, conn, {__MODULE__, {socket, opts}}}
:error ->
send_resp(conn, :forbidden, "")
{:error, conn}
end
_ ->
{:error, conn}
end
end
def init(conn, _) do
send_resp(conn, :bad_request, "")
{:error, conn}
end
def ws_init({socket, config}) do
Process.flag(:trap_exit, true)
{:ok, %{socket: socket}, config[:timeout]}
end
def ws_handle(op, data, state) do
state.socket.handler
|> apply(:handle, [op, data, state])
|> case do
{op, data} ->
{:reply, {op, data}, state}
{op, data, state} ->
{:reply, {op, data}, state}
%{} = state ->
{:ok, state}
_ ->
{:ok, state}
end
end
def ws_info({_, _} = tuple, state) do
{:reply, tuple, state}
end
def ws_info(_tuple, state), do: {:ok, state}
def ws_close(state) do
ws_handle(:closed, :normal, state)
end
def ws_terminate(reason, state) do
ws_handle(:closed, reason, state)
end
end

View File

@ -28,7 +28,7 @@ defp get_cache_keys_for(activity_id) do
end end
end end
defp add_cache_key_for(activity_id, additional_key) do def add_cache_key_for(activity_id, additional_key) do
current = get_cache_keys_for(activity_id) current = get_cache_keys_for(activity_id)
unless additional_key in current do unless additional_key in current do

View File

@ -23,19 +23,21 @@ defmodule Pleroma.Announcement do
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end
def change(struct, params \\ %{}) do @doc "Generates changeset for %Pleroma.Announcement{}"
struct @spec changeset(%__MODULE__{}, map()) :: %Ecto.Changeset{}
|> cast(validate_params(struct, params), [:data, :starts_at, :ends_at, :rendered]) def changeset(announcement \\ %__MODULE__{}, params \\ %{data: %{}}) do
announcement
|> cast(validate_params(announcement, params), [:data, :starts_at, :ends_at, :rendered])
|> validate_required([:data]) |> validate_required([:data])
end end
defp validate_params(struct, params) do defp validate_params(announcement, params) do
base_data = base_data =
%{ %{
"content" => "", "content" => "",
"all_day" => false "all_day" => false
} }
|> Map.merge((struct && struct.data) || %{}) |> Map.merge((announcement && announcement.data) || %{})
merged_data = merged_data =
Map.merge(base_data, params.data) Map.merge(base_data, params.data)
@ -61,13 +63,13 @@ def add_rendered_properties(params) do
end end
def add(params) do def add(params) do
changeset = change(%__MODULE__{}, params) changeset = changeset(%__MODULE__{}, params)
Repo.insert(changeset) Repo.insert(changeset)
end end
def update(announcement, params) do def update(announcement, params) do
changeset = change(announcement, params) changeset = changeset(announcement, params)
Repo.update(changeset) Repo.update(changeset)
end end

View File

@ -8,10 +8,13 @@ defmodule Pleroma.Caching do
@callback put(Cachex.cache(), any(), any(), Keyword.t()) :: {Cachex.status(), boolean()} @callback put(Cachex.cache(), any(), any(), Keyword.t()) :: {Cachex.status(), boolean()}
@callback put(Cachex.cache(), any(), any()) :: {Cachex.status(), boolean()} @callback put(Cachex.cache(), any(), any()) :: {Cachex.status(), boolean()}
@callback fetch!(Cachex.cache(), any(), function() | nil) :: any() @callback fetch!(Cachex.cache(), any(), function() | nil) :: any()
@callback fetch(Cachex.cache(), any(), function() | nil) ::
{atom(), any()} | {atom(), any(), any()}
# @callback del(Cachex.cache(), any(), Keyword.t()) :: {Cachex.status(), boolean()} # @callback del(Cachex.cache(), any(), Keyword.t()) :: {Cachex.status(), boolean()}
@callback del(Cachex.cache(), any()) :: {Cachex.status(), boolean()} @callback del(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
@callback stream!(Cachex.cache(), any()) :: Enumerable.t() @callback stream!(Cachex.cache(), any()) :: Enumerable.t()
@callback expire_at(Cachex.cache(), binary(), number()) :: {Cachex.status(), boolean()} @callback expire_at(Cachex.cache(), binary(), number()) :: {Cachex.status(), boolean()}
@callback expire(Cachex.cache(), binary(), number()) :: {Cachex.status(), boolean()}
@callback exists?(Cachex.cache(), any()) :: {Cachex.status(), boolean()} @callback exists?(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
@callback execute!(Cachex.cache(), function()) :: any() @callback execute!(Cachex.cache(), function()) :: any()
@callback get_and_update(Cachex.cache(), any(), function()) :: @callback get_and_update(Cachex.cache(), any(), function()) ::

View File

@ -256,7 +256,7 @@ def check_old_mrf_config do
move_namespace_and_warn(@mrf_config_map, warning_preface) move_namespace_and_warn(@mrf_config_map, warning_preface)
end end
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | :error
def move_namespace_and_warn(config_map, warning_preface) do def move_namespace_and_warn(config_map, warning_preface) do
warning = warning =
Enum.reduce(config_map, "", fn Enum.reduce(config_map, "", fn
@ -279,7 +279,7 @@ def move_namespace_and_warn(config_map, warning_preface) do
end end
end end
@spec check_media_proxy_whitelist_config() :: :ok | nil @spec check_media_proxy_whitelist_config() :: :ok | :error
def check_media_proxy_whitelist_config do def check_media_proxy_whitelist_config do
whitelist = Config.get([:media_proxy, :whitelist]) whitelist = Config.get([:media_proxy, :whitelist])
@ -340,7 +340,7 @@ def check_gun_pool_options do
end end
end end
@spec check_activity_expiration_config() :: :ok | nil @spec check_activity_expiration_config() :: :ok | :error
def check_activity_expiration_config do def check_activity_expiration_config do
warning_preface = """ warning_preface = """
!!!DEPRECATION WARNING!!! !!!DEPRECATION WARNING!!!
@ -356,7 +356,7 @@ def check_activity_expiration_config do
) )
end end
@spec check_remote_ip_plug_name() :: :ok | nil @spec check_remote_ip_plug_name() :: :ok | :error
def check_remote_ip_plug_name do def check_remote_ip_plug_name do
warning_preface = """ warning_preface = """
!!!DEPRECATION WARNING!!! !!!DEPRECATION WARNING!!!
@ -372,7 +372,7 @@ def check_remote_ip_plug_name do
) )
end end
@spec check_uploaders_s3_public_endpoint() :: :ok | nil @spec check_uploaders_s3_public_endpoint() :: :ok | :error
def check_uploaders_s3_public_endpoint do def check_uploaders_s3_public_endpoint do
s3_config = Pleroma.Config.get([Pleroma.Uploaders.S3]) s3_config = Pleroma.Config.get([Pleroma.Uploaders.S3])
@ -393,7 +393,7 @@ def check_uploaders_s3_public_endpoint do
end end
end end
@spec check_old_chat_shoutbox() :: :ok | nil @spec check_old_chat_shoutbox() :: :ok | :error
def check_old_chat_shoutbox do def check_old_chat_shoutbox do
instance_config = Pleroma.Config.get([:instance]) instance_config = Pleroma.Config.get([:instance])
chat_config = Pleroma.Config.get([:chat]) || [] chat_config = Pleroma.Config.get([:chat]) || []

View File

@ -21,7 +21,7 @@ def load(config, opts) do
with_runtime_config = with_runtime_config =
if File.exists?(config_path) do if File.exists?(config_path) do
# <https://git.pleroma.social/pleroma/pleroma/-/issues/3135> # <https://git.pleroma.social/pleroma/pleroma/-/issues/3135>
%File.Stat{mode: mode} = File.lstat!(config_path) %File.Stat{mode: mode} = File.stat!(config_path)
if Bitwise.band(mode, 0o007) > 0 do if Bitwise.band(mode, 0o007) > 0 do
raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}" raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}"

View File

@ -57,7 +57,7 @@ def maybe_create_recipientships(participation, activity) do
3. Bump all relevant participations to 'unread' 3. Bump all relevant participations to 'unread'
""" """
def create_or_bump_for(activity, opts \\ []) do def create_or_bump_for(activity, opts \\ []) do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), with true <- Pleroma.Web.ActivityPub.Visibility.direct?(activity),
"Create" <- activity.data["type"], "Create" <- activity.data["type"],
%Object{} = object <- Object.normalize(activity, fetch: false), %Object{} = object <- Object.normalize(activity, fetch: false),
true <- object.data["type"] in ["Note", "Question"], true <- object.data["type"] in ["Note", "Question"],

View File

@ -12,6 +12,8 @@ defmodule Pleroma.DataMigration do
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
@type t :: %__MODULE__{}
schema "data_migrations" do schema "data_migrations" do
field(:name, :string) field(:name, :string)
field(:state, State, default: :pending) field(:state, State, default: :pending)

View File

@ -51,12 +51,12 @@ def reload do
end end
@doc "Returns the path of the emoji `name`." @doc "Returns the path of the emoji `name`."
@spec get(String.t()) :: String.t() | nil @spec get(String.t()) :: Pleroma.Emoji.t() | nil
def get(name) do def get(name) do
name = maybe_strip_name(name) name = maybe_strip_name(name)
case :ets.lookup(@ets, name) do case :ets.lookup(@ets, name) do
[{_, path}] -> path [{_, emoji}] -> emoji
_ -> nil _ -> nil
end end
end end
@ -138,23 +138,23 @@ defp update_emojis(emojis) do
emojis = emojis ++ regional_indicators emojis = emojis ++ regional_indicators
for emoji <- emojis do for emoji <- emojis do
def is_unicode_emoji?(unquote(emoji)), do: true def unicode?(unquote(emoji)), do: true
end end
def is_unicode_emoji?(_), do: false def unicode?(_), do: false
@emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/ @emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/
def is_custom_emoji?(s) when is_binary(s), do: Regex.match?(@emoji_regex, s) def custom?(s) when is_binary(s), do: Regex.match?(@emoji_regex, s)
def is_custom_emoji?(_), do: false def custom?(_), do: false
def maybe_strip_name(name) when is_binary(name), do: String.trim(name, ":") def maybe_strip_name(name) when is_binary(name), do: String.trim(name, ":")
def maybe_strip_name(name), do: name def maybe_strip_name(name), do: name
def maybe_quote(name) when is_binary(name) do def maybe_quote(name) when is_binary(name) do
if is_unicode_emoji?(name) do if unicode?(name) do
name name
else else
if String.starts_with?(name, ":") do if String.starts_with?(name, ":") do

View File

@ -15,8 +15,6 @@ defmodule Pleroma.Emoji.Loader do
require Logger require Logger
@mix_env Mix.env()
@type pattern :: Regex.t() | module() | String.t() @type pattern :: Regex.t() | module() | String.t()
@type patterns :: pattern() | [pattern()] @type patterns :: pattern() | [pattern()]
@type group_patterns :: keyword(patterns()) @type group_patterns :: keyword(patterns())
@ -79,7 +77,7 @@ def load do
# for testing emoji.txt entries we do not want exposed in normal operation # for testing emoji.txt entries we do not want exposed in normal operation
test_emoji = test_emoji =
if @mix_env == :test do if Application.get_env(:pleroma, __MODULE__)[:test_emoji] do
load_from_file("test/config/emoji.txt", emoji_groups) load_from_file("test/config/emoji.txt", emoji_groups)
else else
[] []

View File

@ -100,7 +100,7 @@ def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"}
{:ok, _emoji_files} = {:ok, _emoji_files} =
:zip.unzip( :zip.unzip(
to_charlist(file.path), to_charlist(file.path),
[{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}] [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, String.to_charlist(tmp_dir)}]
) )
{_, updated_pack} = {_, updated_pack} =

View File

@ -216,9 +216,6 @@ def compose_regex([_ | _] = filters, format) do
:re -> :re ->
~r/\b#{phrases}\b/i ~r/\b#{phrases}\b/i
_ ->
nil
end end
end end

View File

@ -114,7 +114,7 @@ def response("/main/all") do
def response("/notices/" <> id) do def response("/notices/" <> id) do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.is_public?(activity) do true <- Visibility.public?(activity) do
activities = activities =
ActivityPub.fetch_activities_for_context(activity.data["context"]) ActivityPub.fetch_activities_for_context(activity.data["context"])
|> render_activities |> render_activities

View File

@ -9,7 +9,7 @@ defp registry, do: Pleroma.Gun.ConnectionPool
def start_monitor do def start_monitor do
pid = pid =
case :gen_server.start(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do case GenServer.start_link(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do
{:ok, pid} -> {:ok, pid} ->
pid pid

View File

@ -21,7 +21,9 @@ def init(_opts) do
def start_worker(opts, retry \\ false) do def start_worker(opts, retry \\ false) do
case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
{:error, :max_children} -> {:error, :max_children} ->
if retry or free_pool() == :error do funs = [fn -> !retry end, fn -> match?(:error, free_pool()) end]
if Enum.any?(funs, fn fun -> fun.() end) do
:telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts}) :telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
{:error, :pool_full} {:error, :pool_full}
else else

View File

@ -43,89 +43,28 @@ def image_resize(url, options) do
def video_framegrab(url) do def video_framegrab(url) do
with executable when is_binary(executable) <- System.find_executable("ffmpeg"), with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
{:ok, env} <- HTTP.get(url, [], pool: :media), {:ok, env} <- HTTP.get(url, [], pool: :media),
{:ok, fifo_path} <- mkfifo(), {:ok, pid} <- StringIO.open(env.body) do
args = [ body_stream = IO.binstream(pid, 1)
"-y",
"-i", Exile.stream!(
fifo_path, [
"-vframes", executable,
"1", "-i",
"-f", "pipe:0",
"mjpeg", "-vframes",
"-loglevel", "1",
"error", "-f",
"-" "mjpeg",
] do "pipe:1"
run_fifo(fifo_path, env, executable, args) ],
input: body_stream,
ignore_epipe: true,
stderr: :disable
)
|> Enum.into(<<>>)
else else
nil -> {:error, {:ffmpeg, :command_not_found}} nil -> {:error, {:ffmpeg, :command_not_found}}
{:error, _} = error -> error {:error, _} = error -> error
end end
end end
defp run_fifo(fifo_path, env, executable, args) do
pid =
Port.open({:spawn_executable, executable}, [
:use_stdio,
:stream,
:exit_status,
:binary,
args: args
])
fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out])
fix = Pleroma.Helpers.QtFastStart.fix(env.body)
true = Port.command(fifo, fix)
:erlang.port_close(fifo)
loop_recv(pid)
after
File.rm(fifo_path)
end
defp mkfifo do
path = Path.join(System.tmp_dir!(), "pleroma-media-preview-pipe-#{Ecto.UUID.generate()}")
case System.cmd("mkfifo", [path]) do
{_, 0} ->
spawn(fifo_guard(path))
{:ok, path}
{_, err} ->
{:error, {:fifo_failed, err}}
end
end
defp fifo_guard(path) do
pid = self()
fn ->
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, _} ->
File.rm(path)
end
end
end
defp loop_recv(pid) do
loop_recv(pid, <<>>)
end
defp loop_recv(pid, acc) do
receive do
{^pid, {:data, data}} ->
loop_recv(pid, acc <> data)
{^pid, {:exit_status, 0}} ->
{:ok, acc}
{^pid, {:exit_status, status}} ->
{:error, status}
after
5000 ->
:erlang.port_close(pid)
{:error, :timeout}
end
end
end end

View File

@ -126,9 +126,15 @@ defp rewrite_entries(
<<pos::integer-big-size(unquote(size)), rest::bits>>, <<pos::integer-big-size(unquote(size)), rest::bits>>,
acc acc
) do ) do
rewrite_entries(unquote(size), offset, rest, [ rewrite_entries(
acc | <<pos + offset::integer-big-size(unquote(size))>> unquote(size),
]) offset,
rest,
acc ++
[
<<pos + offset::integer-big-size(unquote(size))>>
]
)
end end
end end

View File

@ -6,8 +6,6 @@ defmodule Pleroma.HTML do
# Scrubbers are compiled on boot so they can be configured in OTP releases # Scrubbers are compiled on boot so they can be configured in OTP releases
# @on_load :compile_scrubbers # @on_load :compile_scrubbers
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def compile_scrubbers do def compile_scrubbers do
dir = Path.join(:code.priv_dir(:pleroma), "scrubbers") dir = Path.join(:code.priv_dir(:pleroma), "scrubbers")
@ -67,27 +65,20 @@ def ensure_scrubbed_html(
end end
end end
def extract_first_external_url_from_object(%{data: %{"content" => content}} = object) @spec extract_first_external_url_from_object(Pleroma.Object.t()) ::
{:ok, String.t()} | {:error, :no_content}
def extract_first_external_url_from_object(%{data: %{"content" => content}})
when is_binary(content) do when is_binary(content) do
unless object.data["fake"] do url =
key = "URL|#{object.id}" content
|> Floki.parse_fragment!()
|> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])")
|> Enum.take(1)
|> Floki.attribute("href")
|> Enum.at(0)
@cachex.fetch!(:scrubber_cache, key, fn _key -> {:ok, url}
{:commit, {:ok, extract_first_external_url(content)}}
end)
else
{:ok, extract_first_external_url(content)}
end
end end
def extract_first_external_url_from_object(_), do: {:error, :no_content} def extract_first_external_url_from_object(_), do: {:error, :no_content}
def extract_first_external_url(content) do
content
|> Floki.parse_fragment!()
|> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])")
|> Enum.take(1)
|> Floki.attribute("href")
|> Enum.at(0)
end
end end

View File

@ -15,8 +15,8 @@ defmodule Pleroma.HTTP.AdapterHelper do
require Logger require Logger
@type proxy :: @type proxy ::
{Connection.host(), pos_integer()} {host(), pos_integer()}
| {Connection.proxy_type(), Connection.host(), pos_integer()} | {proxy_type(), host(), pos_integer()}
@callback options(keyword(), URI.t()) :: keyword() @callback options(keyword(), URI.t()) :: keyword()

View File

@ -54,12 +54,12 @@ def opts(request, options), do: %{request | opts: options}
@doc """ @doc """
Add optional parameters to the request Add optional parameters to the request
""" """
@spec add_param(Request.t(), atom(), atom(), any()) :: Request.t() @spec add_param(Request.t(), atom(), atom() | String.t(), any()) :: Request.t()
def add_param(request, :query, :query, values), do: %{request | query: values} def add_param(request, :query, :query, values), do: %{request | query: values}
def add_param(request, :body, :body, value), do: %{request | body: value} def add_param(request, :body, :body, value), do: %{request | body: value}
def add_param(request, :body, key, value) do def add_param(request, :body, key, value) when is_binary(key) do
request request
|> Map.put(:body, Multipart.new()) |> Map.put(:body, Multipart.new())
|> Map.update!( |> Map.update!(

View File

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server # Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Maps do defmodule Pleroma.Maps do
@ -18,4 +18,17 @@ def safe_put_in(data, keys, value) when is_map(data) and is_list(keys) do
rescue rescue
_ -> data _ -> data
end end
def filter_empty_values(data) do
# TODO: Change to Map.filter in Elixir 1.13+
data
|> Enum.filter(fn
{_k, nil} -> false
{_k, ""} -> false
{_k, []} -> false
{_k, %{} = v} -> Map.keys(v) != []
{_k, _v} -> true
end)
|> Map.new()
end
end end

View File

@ -77,7 +77,7 @@ def generate_backup_codes(%User{} = user) do
{:ok, codes} {:ok, codes}
else else
{:error, msg} -> {:error, msg} ->
%{error: msg} {:error, msg}
end end
end end

View File

@ -14,6 +14,7 @@ defmodule Pleroma.MFA.TOTP do
@doc """ @doc """
https://github.com/google/google-authenticator/wiki/Key-Uri-Format https://github.com/google/google-authenticator/wiki/Key-Uri-Format
""" """
@spec provisioning_uri(String.t(), String.t(), list()) :: String.t()
def provisioning_uri(secret, label, opts \\ []) do def provisioning_uri(secret, label, opts \\ []) do
query = query =
%{ %{
@ -27,7 +28,7 @@ def provisioning_uri(secret, label, opts \\ []) do
|> URI.encode_query() |> URI.encode_query()
%URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query} %URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
|> URI.to_string() |> to_string()
end end
defp default_period, do: Config.get(@config_ns ++ [:period]) defp default_period, do: Config.get(@config_ns ++ [:period])

View File

@ -188,10 +188,11 @@ defp update_status(status, message \\ nil) do
end end
defp fault_rate do defp fault_rate do
with failures_count when is_integer(failures_count) <- failures_count() do with failures_count when is_integer(failures_count) <- failures_count(),
true <- failures_count > 0 do
failures_count / Enum.max([get_stat(:affected_count, 0), 1]) failures_count / Enum.max([get_stat(:affected_count, 0), 1])
else else
_ -> :error _ -> 0
end end
end end

View File

@ -121,7 +121,7 @@ defp prepare_log_data(%{actor: actor, action: action} = attrs) do
defp prepare_log_data(attrs), do: attrs defp prepare_log_data(attrs), do: attrs
@spec insert_log(log_params()) :: {:ok, ModerationLog} | {:error, any} @spec insert_log(log_params()) :: {:ok, ModerationLog.t()} | {:error, any}
def insert_log(%{actor: %User{}, subject: subjects, permission: permission} = attrs) do def insert_log(%{actor: %User{}, subject: subjects, permission: permission} = attrs) do
data = data =
attrs attrs
@ -248,7 +248,8 @@ def insert_log(%{actor: %User{} = actor, action: "chat_message_delete", subject_
|> insert_log_entry_with_message() |> insert_log_entry_with_message()
end end
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any} @spec insert_log_entry_with_message(ModerationLog.t()) ::
{:ok, ModerationLog.t()} | {:error, any}
defp insert_log_entry_with_message(entry) do defp insert_log_entry_with_message(entry) do
entry.data["message"] entry.data["message"]
|> put_in(get_log_entry_message(entry)) |> put_in(get_log_entry_message(entry))

View File

@ -88,7 +88,7 @@ def last_read_query(user) do
where: q.seen == true, where: q.seen == true,
select: type(q.id, :string), select: type(q.id, :string),
limit: 1, limit: 1,
order_by: [desc: :id] order_by: fragment("? desc nulls last", q.id)
) )
end end

View File

@ -242,17 +242,17 @@ def delete(%Object{data: %{"id" => id}} = object) do
{:ok, _} <- invalid_object_cache(object) do {:ok, _} <- invalid_object_cache(object) do
cleanup_attachments( cleanup_attachments(
Config.get([:instance, :cleanup_attachments]), Config.get([:instance, :cleanup_attachments]),
%{"object" => object} object
) )
{:ok, object, deleted_activity} {:ok, object, deleted_activity}
end end
end end
@spec cleanup_attachments(boolean(), %{required(:object) => map()}) :: @spec cleanup_attachments(boolean(), Object.t()) ::
{:ok, Oban.Job.t() | nil} {:ok, Oban.Job.t() | nil}
def cleanup_attachments(true, %{"object" => _} = params) do def cleanup_attachments(true, %Object{} = object) do
AttachmentsCleanupWorker.enqueue("cleanup_attachments", params) AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{"object" => object})
end end
def cleanup_attachments(_, _), do: {:ok, nil} def cleanup_attachments(_, _), do: {:ok, nil}

View File

@ -61,15 +61,16 @@ def fetch_paginated(query, params, :offset, table_binding) do
|> Repo.all() |> Repo.all()
end end
@spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()] @spec paginate_list(list(), keyword()) :: list()
def paginate(query, options, method \\ :keyset, table_binding \\ nil) def paginate_list(list, options) do
def paginate(list, options, _method, _table_binding) when is_list(list) do
offset = options[:offset] || 0 offset = options[:offset] || 0
limit = options[:limit] || 0 limit = options[:limit] || 0
Enum.slice(list, offset, limit) Enum.slice(list, offset, limit)
end end
@spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
def paginate(query, options, method \\ :keyset, table_binding \\ nil)
def paginate(query, options, :keyset, table_binding) do def paginate(query, options, :keyset, table_binding) do
query query
|> restrict(:min_id, options, table_binding) |> restrict(:min_id, options, table_binding)

View File

@ -28,7 +28,7 @@ def verify_pass(password, hash) do
iterations = String.to_integer(iterations) iterations = String.to_integer(iterations)
digest = String.to_atom(digest) digest = String.to_existing_atom(digest)
binary_hash = binary_hash =
KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64) KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)

View File

@ -8,7 +8,7 @@ defmodule Pleroma.ReverseProxy do
~w(if-unmodified-since if-none-match) ++ @range_headers ~w(if-unmodified-since if-none-match) ++ @range_headers
@resp_cache_headers ~w(etag date last-modified) @resp_cache_headers ~w(etag date last-modified)
@keep_resp_headers @resp_cache_headers ++ @keep_resp_headers @resp_cache_headers ++
~w(content-length content-type content-disposition content-encoding) ++ ~w(content-type content-disposition content-encoding) ++
~w(content-range accept-ranges vary) ~w(content-range accept-ranges vary)
@default_cache_control_header "public, max-age=1209600" @default_cache_control_header "public, max-age=1209600"
@valid_resp_codes [200, 206, 304] @valid_resp_codes [200, 206, 304]
@ -84,13 +84,13 @@ def default_cache_control_header, do: @default_cache_control_header
{:max_read_duration, non_neg_integer() | :infinity} {:max_read_duration, non_neg_integer() | :infinity}
| {:max_body_length, non_neg_integer() | :infinity} | {:max_body_length, non_neg_integer() | :infinity}
| {:failed_request_ttl, non_neg_integer() | :infinity} | {:failed_request_ttl, non_neg_integer() | :infinity}
| {:http, []} | {:http, keyword()}
| {:req_headers, [{String.t(), String.t()}]} | {:req_headers, [{String.t(), String.t()}]}
| {:resp_headers, [{String.t(), String.t()}]} | {:resp_headers, [{String.t(), String.t()}]}
| {:inline_content_types, boolean() | [String.t()]} | {:inline_content_types, boolean() | list(String.t())}
| {:redirect_on_failure, boolean()} | {:redirect_on_failure, boolean()}
@spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t() @spec call(Plug.Conn.t(), String.t(), list(option())) :: Plug.Conn.t()
def call(_conn, _url, _opts \\ []) def call(_conn, _url, _opts \\ [])
def call(conn = %{method: method}, url, opts) when method in @methods do def call(conn = %{method: method}, url, opts) when method in @methods do
@ -388,8 +388,6 @@ defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and
defp body_size_constraint(_, _), do: :ok defp body_size_constraint(_, _), do: :ok
defp check_read_duration(nil = _duration, max), do: check_read_duration(@max_read_duration, max)
defp check_read_duration(duration, max) defp check_read_duration(duration, max)
when is_integer(duration) and is_integer(max) and max > 0 do when is_integer(duration) and is_integer(max) and max > 0 do
if duration > max do if duration > max do
@ -407,10 +405,6 @@ defp increase_read_duration({previous_duration, started})
{:ok, previous_duration + duration} {:ok, previous_duration + duration}
end end
defp increase_read_duration(_) do
{:ok, :no_duration_limit, :no_duration_limit}
end
defp client, do: Pleroma.ReverseProxy.Client.Wrapper defp client, do: Pleroma.ReverseProxy.Client.Wrapper
defp track_failed_url(url, error, opts) do defp track_failed_url(url, error, opts) do

View File

@ -20,5 +20,5 @@ defmodule Pleroma.Search.SearchBackend do
is what contains the actual content and there is no need for filtering when removing is what contains the actual content and there is no need for filtering when removing
from index. from index.
""" """
@callback remove_from_index(object :: Pleroma.Object.t()) :: {:ok, any()} | {:error, any()} @callback remove_from_index(object :: Pleroma.Object.t()) :: :ok | {:error, any()}
end end

View File

@ -27,7 +27,7 @@ def key_id_to_actor_id(key_id) do
_ -> _ ->
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
%{"ap_id" => ap_id} -> {:ok, ap_id} {:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id}
_ -> {:error, maybe_ap_id} _ -> {:error, maybe_ap_id}
end end
end end

View File

@ -59,7 +59,7 @@ def handle_event(
_, _,
_ _
) do ) do
Logger.error(fn -> Logger.debug(fn ->
"Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion" "Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion"
end) end)
end end
@ -81,7 +81,7 @@ def handle_event(
%{key: key, protocol: :http}, %{key: key, protocol: :http},
_ _
) do ) do
Logger.info(fn -> Logger.debug(fn ->
"Pool worker for #{key}: #{length(clients)} clients are using an HTTP1 connection at the same time, head-of-line blocking might occur." "Pool worker for #{key}: #{length(clients)} clients are using an HTTP1 connection at the same time, head-of-line blocking might occur."
end) end)
end end

View File

@ -51,6 +51,7 @@ defmodule Pleroma.Upload do
| {:size_limit, nil | non_neg_integer()} | {:size_limit, nil | non_neg_integer()}
| {:uploader, module()} | {:uploader, module()}
| {:filters, [module()]} | {:filters, [module()]}
| {:actor, String.t()}
@type t :: %__MODULE__{ @type t :: %__MODULE__{
id: String.t(), id: String.t(),
@ -175,7 +176,7 @@ defp prepare_upload(%Plug.Upload{} = file, opts) do
defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data) parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace) data = Base.decode64!(parsed["data"], ignore: :whitespace)
hash = Base.encode16(:crypto.hash(:sha256, data), lower: true) hash = Base.encode16(:crypto.hash(:sha256, data), case: :upper)
with :ok <- check_binary_size(data, opts.size_limit), with :ok <- check_binary_size(data, opts.size_limit),
tmp_path <- tempfile_for_image(data), tmp_path <- tempfile_for_image(data),

View File

@ -5,8 +5,6 @@
defmodule Pleroma.Uploaders.Uploader do defmodule Pleroma.Uploaders.Uploader do
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
@mix_env Mix.env()
@moduledoc """ @moduledoc """
Defines the contract to put and get an uploaded file to any backend. Defines the contract to put and get an uploaded file to any backend.
""" """
@ -75,10 +73,5 @@ defp handle_callback(uploader, upload) do
end end
end end
defp callback_timeout do defp callback_timeout, do: Application.get_env(:pleroma, __MODULE__)[:timeout]
case @mix_env do
:test -> 1_000
_ -> 30_000
end
end
end end

View File

@ -1787,7 +1787,10 @@ def set_activation_async(user, status \\ true) do
@spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def set_activation(users, status) when is_list(users) do def set_activation(users, status) when is_list(users) do
Repo.transaction(fn -> Repo.transaction(fn ->
for user <- users, do: set_activation(user, status) for user <- users do
{:ok, user} = set_activation(user, status)
user
end
end) end)
end end
@ -2404,9 +2407,9 @@ defp put_password_hash(
defp put_password_hash(changeset), do: changeset defp put_password_hash(changeset), do: changeset
def is_internal_user?(%User{nickname: nil}), do: true def internal?(%User{nickname: nil}), do: true
def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true def internal?(%User{local: true, nickname: "internal." <> _}), do: true
def is_internal_user?(_), do: false def internal?(_), do: false
# A hack because user delete activities have a fake id for whatever reason # A hack because user delete activities have a fake id for whatever reason
# TODO: Get rid of this # TODO: Get rid of this
@ -2556,9 +2559,9 @@ def confirmation_changeset(user, set_confirmation: confirmed?) do
cast(user, params, [:is_confirmed, :confirmation_token]) cast(user, params, [:is_confirmed, :confirmation_token])
end end
@spec approval_changeset(User.t(), keyword()) :: Ecto.Changeset.t() @spec approval_changeset(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
def approval_changeset(user, set_approval: approved?) do def approval_changeset(changeset, set_approval: approved?) do
cast(user, %{is_approved: approved?}, [:is_approved]) cast(changeset, %{is_approved: approved?}, [:is_approved])
end end
@spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()} @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}

View File

@ -22,6 +22,8 @@ defmodule Pleroma.User.Backup do
alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Workers.BackupWorker alias Pleroma.Workers.BackupWorker
@type t :: %__MODULE__{}
schema "backups" do schema "backups" do
field(:content_type, :string) field(:content_type, :string)
field(:file_name, :string) field(:file_name, :string)
@ -195,6 +197,7 @@ defp wait_backup(backup, current_processed, task) do
end end
@files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
@spec export(Pleroma.User.Backup.t(), pid()) :: {:ok, String.t()} | :error
def export(%__MODULE__{} = backup, caller_pid) do def export(%__MODULE__{} = backup, caller_pid) do
backup = Repo.preload(backup, :user) backup = Repo.preload(backup, :user)
dir = backup_tempdir(backup) dir = backup_tempdir(backup)
@ -204,9 +207,11 @@ def export(%__MODULE__{} = backup, caller_pid) do
:ok <- statuses(dir, backup.user, caller_pid), :ok <- statuses(dir, backup.user, caller_pid),
:ok <- likes(dir, backup.user, caller_pid), :ok <- likes(dir, backup.user, caller_pid),
:ok <- bookmarks(dir, backup.user, caller_pid), :ok <- bookmarks(dir, backup.user, caller_pid),
{:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir), {:ok, zip_path} <- :zip.create(backup.file_name, @files, cwd: dir),
{:ok, _} <- File.rm_rf(dir) do {:ok, _} <- File.rm_rf(dir) do
{:ok, to_string(zip_path)} {:ok, zip_path}
else
_ -> :error
end end
end end
@ -382,6 +387,8 @@ def do_process(backup, current_pid) do
[:file_size, :processed, :state] [:file_size, :processed, :state]
) )
|> Repo.update() |> Repo.update()
else
e -> {:error, e}
end end
end end
end end

View File

@ -71,7 +71,7 @@ defmodule Pleroma.User.Query do
@equal_criteria [:email] @equal_criteria [:email]
@contains_criteria [:ap_id, :nickname] @contains_criteria [:ap_id, :nickname]
@spec build(Query.t(), criteria()) :: Query.t() @spec build(Ecto.Query.t(), criteria()) :: Ecto.Query.t()
def build(query \\ base_query(), criteria) do def build(query \\ base_query(), criteria) do
prepare_query(query, criteria) prepare_query(query, criteria)
end end

View File

@ -14,6 +14,8 @@ defmodule Pleroma.UserRelationship do
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
@type t :: %__MODULE__{}
schema "user_relationships" do schema "user_relationships" do
belongs_to(:source, User, type: FlakeId.Ecto.CompatType) belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
belongs_to(:target, User, type: FlakeId.Ecto.CompatType) belongs_to(:target, User, type: FlakeId.Ecto.CompatType)

View File

@ -74,22 +74,22 @@ defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(
defp check_remote_limit(_), do: true defp check_remote_limit(_), do: true
def increase_note_count_if_public(actor, object) do def increase_note_count_if_public(actor, object) do
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor} if public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end end
def decrease_note_count_if_public(actor, object) do def decrease_note_count_if_public(actor, object) do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor} if public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end end
def update_last_status_at_if_public(actor, object) do def update_last_status_at_if_public(actor, object) do
if is_public?(object), do: User.update_last_status_at(actor), else: {:ok, actor} if public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
end end
defp increase_replies_count_if_reply(%{ defp increase_replies_count_if_reply(%{
"object" => %{"inReplyTo" => reply_ap_id} = object, "object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create" "type" => "Create"
}) do }) do
if is_public?(object) do if public?(object) do
Object.increase_replies_count(reply_ap_id) Object.increase_replies_count(reply_ap_id)
end end
end end
@ -100,7 +100,7 @@ defp increase_quotes_count_if_quote(%{
"object" => %{"quoteUrl" => quote_ap_id} = object, "object" => %{"quoteUrl" => quote_ap_id} = object,
"type" => "Create" "type" => "Create"
}) do }) do
if is_public?(object) do if public?(object) do
Object.increase_quotes_count(quote_ap_id) Object.increase_quotes_count(quote_ap_id)
end end
end end

View File

@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
This module encodes our addressing policies and general shape of our objects. This module encodes our addressing policies and general shape of our objects.
""" """
alias Pleroma.Activity
alias Pleroma.Emoji alias Pleroma.Emoji
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
@ -131,7 +132,7 @@ defp custom_emoji_react(object, data, emoji) do
def emoji_react(actor, object, emoji) do def emoji_react(actor, object, emoji) do
with {:ok, data, meta} <- object_action(actor, object) do with {:ok, data, meta} <- object_action(actor, object) do
data = data =
if Emoji.is_unicode_emoji?(emoji) do if Emoji.unicode?(emoji) do
unicode_emoji_react(object, data, emoji) unicode_emoji_react(object, data, emoji)
else else
custom_emoji_react(object, data, emoji) custom_emoji_react(object, data, emoji)
@ -347,7 +348,7 @@ def announce(actor, object, options \\ []) do
actor.ap_id == Relay.ap_id() -> actor.ap_id == Relay.ap_id() ->
[actor.follower_address] [actor.follower_address]
public? and Visibility.is_local_public?(object) -> public? and Visibility.local_public?(object) ->
[actor.follower_address, object.data["actor"], Utils.as_local_public()] [actor.follower_address, object.data["actor"], Utils.as_local_public()]
public? -> public? ->
@ -375,7 +376,7 @@ defp object_action(actor, object) do
# Address the actor of the object, and our actor's follower collection if the post is public. # Address the actor of the object, and our actor's follower collection if the post is public.
to = to =
if Visibility.is_public?(object) do if Visibility.public?(object) do
[actor.follower_address, object.data["actor"]] [actor.follower_address, object.data["actor"]]
else else
[object.data["actor"]] [object.data["actor"]]

View File

@ -84,7 +84,7 @@ def filter(%{"type" => type, "object" => object} = message) when type in ["Creat
if hashtags != [] do if hashtags != [] do
with {:ok, message} <- check_reject(message, hashtags), with {:ok, message} <- check_reject(message, hashtags),
{:ok, message} <- {:ok, message} <-
(if "type" == "Create" do (if type == "Create" do
check_ftl_removal(message, hashtags) check_ftl_removal(message, hashtags)
else else
{:ok, message} {:ok, message}

View File

@ -62,7 +62,6 @@ def config_description do
key: :mrf_inline_quote, key: :mrf_inline_quote,
related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy",
label: "MRF Inline Quote Policy", label: "MRF Inline Quote Policy",
type: :group,
description: "Force quote url to appear in post content.", description: "Force quote url to appear in post content.",
children: [ children: [
%{ %{

View File

@ -10,15 +10,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@moduledoc "Reject or Word-Replace messages with a keyword or regex" @moduledoc "Reject or Word-Replace messages with a keyword or regex"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy @behaviour Pleroma.Web.ActivityPub.MRF.Policy
defp string_matches?(string, _) when not is_binary(string) do
false
end
defp string_matches?(string, pattern) when is_binary(pattern) do defp string_matches?(string, pattern) when is_binary(pattern) do
String.contains?(string, pattern) String.contains?(string, pattern)
end end
defp string_matches?(string, pattern) do defp string_matches?(string, %Regex{} = pattern) do
String.match?(string, pattern) String.match?(string, pattern)
end end

View File

@ -10,9 +10,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
@impl true @impl true
def filter(%{"actor" => actor} = object) do def filter(%{"actor" => actor} = object) do
with true <- is_local?(actor), with true <- local?(actor),
true <- is_eligible_type?(object), true <- eligible_type?(object),
true <- is_note?(object), true <- note?(object),
false <- has_attachment?(object), false <- has_attachment?(object),
true <- only_mentions?(object) do true <- only_mentions?(object) do
{:reject, "[NoEmptyPolicy]"} {:reject, "[NoEmptyPolicy]"}
@ -24,7 +24,7 @@ def filter(%{"actor" => actor} = object) do
def filter(object), do: {:ok, object} def filter(object), do: {:ok, object}
defp is_local?(actor) do defp local?(actor) do
if actor |> String.starts_with?("#{Endpoint.url()}") do if actor |> String.starts_with?("#{Endpoint.url()}") do
true true
else else
@ -59,11 +59,11 @@ defp only_mentions?(%{"object" => %{"type" => "Note", "source" => source}}) do
defp only_mentions?(_), do: false defp only_mentions?(_), do: false
defp is_note?(%{"object" => %{"type" => "Note"}}), do: true defp note?(%{"object" => %{"type" => "Note"}}), do: true
defp is_note?(_), do: false defp note?(_), do: false
defp is_eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true defp eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true
defp is_eligible_type?(_), do: false defp eligible_type?(_), do: false
@impl true @impl true
def describe, do: {:ok, %{}} def describe, do: {:ok, %{}}

View File

@ -28,7 +28,7 @@ defp filter_object(%{"quoteUrl" => quote_url} = object) do
tags = object["tag"] || [] tags = object["tag"] || []
if Enum.any?(tags, fn tag -> if Enum.any?(tags, fn tag ->
CommonFixes.is_object_link_tag(tag) and tag["href"] == quote_url CommonFixes.object_link_tag?(tag) and tag["href"] == quote_url
end) do end) do
object object
else else

View File

@ -36,6 +36,7 @@ defp steal_emoji({shortcode, url}, emoji_dir_path) do
extension = if extension == "", do: ".png", else: extension extension = if extension == "", do: ".png", else: extension
shortcode = Path.basename(shortcode)
file_path = Path.join(emoji_dir_path, shortcode <> extension) file_path = Path.join(emoji_dir_path, shortcode <> extension)
case File.write(file_path, response.body) do case File.write(file_path, response.body) do
@ -78,6 +79,7 @@ def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = messa
new_emojis = new_emojis =
foreign_emojis foreign_emojis
|> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end) |> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end)
|> Enum.reject(fn {shortcode, _url} -> String.contains?(shortcode, ["/", "\\"]) end)
|> Enum.filter(fn {shortcode, _url} -> |> Enum.filter(fn {shortcode, _url} ->
reject_emoji? = reject_emoji? =
[:mrf_steal_emoji, :rejected_shortcodes] [:mrf_steal_emoji, :rejected_shortcodes]

View File

@ -173,6 +173,9 @@ def validate(
{:object_validation, e} -> {:object_validation, e} ->
e e
{:error, %Ecto.Changeset{} = e} ->
{:error, e}
end end
end end

View File

@ -82,7 +82,7 @@ defp validate_announcable(cng) do
object when is_binary(object) <- get_field(cng, :object), object when is_binary(object) <- get_field(cng, :object),
%User{} = actor <- User.get_cached_by_ap_id(actor), %User{} = actor <- User.get_cached_by_ap_id(actor),
%Object{} = object <- Object.get_cached_by_ap_id(object), %Object{} = object <- Object.get_cached_by_ap_id(object),
false <- Visibility.is_public?(object) do false <- Visibility.public?(object) do
same_actor = object.data["actor"] == actor.ap_id same_actor = object.data["actor"] == actor.ap_id
recipients = get_field(cng, :to) ++ get_field(cng, :cc) recipients = get_field(cng, :to) ++ get_field(cng, :cc)
local_public = Utils.as_local_public() local_public = Utils.as_local_public()

View File

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Maps
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.User alias Pleroma.User
@ -24,6 +25,8 @@ def cast_and_filter_recipients(message, field, follower_collection, field_fallba
end end
def fix_object_defaults(data) do def fix_object_defaults(data) do
data = Maps.filter_empty_values(data)
context = context =
Utils.maybe_create_context( Utils.maybe_create_context(
data["context"] || data["conversation"] || data["inReplyTo"] || data["id"] data["context"] || data["conversation"] || data["inReplyTo"] || data["id"]
@ -99,7 +102,7 @@ def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do
end end
def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do
tag = Enum.find(tags, &is_object_link_tag/1) tag = Enum.find(tags, &object_link_tag?/1)
if not is_nil(tag) do if not is_nil(tag) do
data data
@ -112,7 +115,7 @@ def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do
def fix_quote_url(data), do: data def fix_quote_url(data), do: data
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
def is_object_link_tag(%{ def object_link_tag?(%{
"type" => "Link", "type" => "Link",
"mediaType" => media_type, "mediaType" => media_type,
"href" => href "href" => href
@ -121,5 +124,5 @@ def is_object_link_tag(%{
true true
end end
def is_object_link_tag(_), do: false def object_link_tag?(_), do: false
end end

View File

@ -74,10 +74,10 @@ defp fix_emoji_qualification(%{"content" => emoji} = data) do
new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji) new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji)
cond do cond do
Pleroma.Emoji.is_unicode_emoji?(emoji) -> Pleroma.Emoji.unicode?(emoji) ->
data data
Pleroma.Emoji.is_unicode_emoji?(new_emoji) -> Pleroma.Emoji.unicode?(new_emoji) ->
data |> Map.put("content", new_emoji) data |> Map.put("content", new_emoji)
true -> true ->
@ -90,7 +90,7 @@ defp fix_emoji_qualification(data), do: data
defp validate_emoji(cng) do defp validate_emoji(cng) do
content = get_field(cng, :content) content = get_field(cng, :content)
if Emoji.is_unicode_emoji?(content) || Emoji.is_custom_emoji?(content) do if Emoji.unicode?(content) || Emoji.custom?(content) do
cng cng
else else
cng cng
@ -101,7 +101,7 @@ defp validate_emoji(cng) do
defp maybe_validate_tag_presence(cng) do defp maybe_validate_tag_presence(cng) do
content = get_field(cng, :content) content = get_field(cng, :content)
if Emoji.is_unicode_emoji?(content) do if Emoji.unicode?(content) do
cng cng
else else
tag = get_field(cng, :tag) tag = get_field(cng, :tag)

View File

@ -62,7 +62,7 @@ defp maybe_federate(%Activity{} = activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do with {:ok, local} <- Keyword.fetch(meta, :local) do
do_not_federate = meta[:do_not_federate] || !config().get([:instance, :federating]) do_not_federate = meta[:do_not_federate] || !config().get([:instance, :federating])
if !do_not_federate and local and not Visibility.is_local_public?(activity) do if !do_not_federate and local and not Visibility.local_public?(activity) do
activity = activity =
if object = Keyword.get(meta, :object_data) do if object = Keyword.get(meta, :object_data) do
%{activity | data: Map.put(activity.data, "object", object)} %{activity | data: Map.put(activity.data, "object", object)}

View File

@ -66,7 +66,7 @@ def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
@doc """ @doc """
Determine if an activity can be represented by running it through Transmogrifier. Determine if an activity can be represented by running it through Transmogrifier.
""" """
def is_representable?(%Activity{} = activity) do def representable?(%Activity{} = activity) do
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
true true
else else
@ -246,7 +246,7 @@ def determine_inbox(
def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity) def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
when is_list(bcc) and bcc != [] do when is_list(bcc) and bcc != [] do
public = is_public?(activity) public = public?(activity)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data) {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
[priority_recipients, recipients] = recipients(actor, activity) [priority_recipients, recipients] = recipients(actor, activity)
@ -291,7 +291,7 @@ def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
# Publishes an activity to all relevant peers. # Publishes an activity to all relevant peers.
def publish(%User{} = actor, %Activity{} = activity) do def publish(%User{} = actor, %Activity{} = activity) do
public = is_public?(activity) public = public?(activity)
if public && Config.get([:instance, :allow_relay]) do if public && Config.get([:instance, :allow_relay]) do
Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end) Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)

View File

@ -58,7 +58,7 @@ defp fetch_target_user(ap_id, opts) do
@spec publish(any()) :: {:ok, Activity.t()} | {:error, any()} @spec publish(any()) :: {:ok, Activity.t()} | {:error, any()}
def publish(%Activity{data: %{"type" => "Create"}} = activity) do def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(), with %User{} = user <- get_actor(),
true <- Visibility.is_public?(activity) do true <- Visibility.public?(activity) do
CommonAPI.repeat(activity.id, user) CommonAPI.repeat(activity.id, user)
else else
error -> format_error(error) error -> format_error(error)

View File

@ -258,7 +258,7 @@ def handle(%{data: %{"type" => "Announce"}} = object, meta) do
Utils.add_announce_to_object(object, announced_object) Utils.add_announce_to_object(object, announced_object)
if !User.is_internal_user?(user) do if !User.internal?(user) do
Notification.create_notifications(object) Notification.create_notifications(object)
ap_streamer().stream_out(object) ap_streamer().stream_out(object)
@ -304,9 +304,9 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
result = result =
case deleted_object do case deleted_object do
%Object{} -> %Object{} ->
with {:ok, deleted_object, _activity} <- Object.delete(deleted_object), with {_, {:ok, deleted_object, _activity}} <- {:object, Object.delete(deleted_object)},
{_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]}, {_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]},
%User{} = user <- User.get_cached_by_ap_id(actor) do {_, %User{} = user} <- {:user, User.get_cached_by_ap_id(actor)} do
User.remove_pinned_object_id(user, deleted_object.data["id"]) User.remove_pinned_object_id(user, deleted_object.data["id"])
{:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object) {:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
@ -328,6 +328,17 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
{:actor, _} -> {:actor, _} ->
@logger.error("The object doesn't have an actor: #{inspect(deleted_object)}") @logger.error("The object doesn't have an actor: #{inspect(deleted_object)}")
:no_object_actor :no_object_actor
{:user, _} ->
@logger.error(
"The object's actor could not be resolved to a user: #{inspect(deleted_object)}"
)
:no_object_user
{:object, _} ->
@logger.error("The object could not be deleted: #{inspect(deleted_object)}")
{:error, object}
end end
%User{} -> %User{} ->
@ -569,7 +580,7 @@ def handle_undoing(
def handle_undoing(object), do: {:error, ["don't know how to handle", object]} def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
@spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()} @spec delete_object(Activity.t()) :: :ok | {:error, Ecto.Changeset.t()}
defp delete_object(object) do defp delete_object(object) do
with {:ok, _} <- Repo.delete(object), do: :ok with {:ok, _} <- Repo.delete(object), do: :ok
end end

View File

@ -4,5 +4,5 @@
defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do
@callback handle(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} @callback handle(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
@callback handle_after_transaction(map()) :: map() @callback handle_after_transaction(keyword()) :: keyword()
end end

View File

@ -336,10 +336,6 @@ def fix_tag(%{"tag" => %{} = tag} = object) do
def fix_tag(object), do: object def fix_tag(object), do: object
def fix_content_map(%{"contentMap" => nil} = object) do
Map.drop(object, ["contentMap"])
end
# content map usually only has one language so this will do for now. # content map usually only has one language so this will do for now.
def fix_content_map(%{"contentMap" => content_map} = object) do def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map) content_groups = Map.to_list(content_map)
@ -783,7 +779,7 @@ def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => objec
|> Object.normalize(fetch: false) |> Object.normalize(fetch: false)
data = data =
if Visibility.is_private?(object) && object.data["actor"] == ap_id do if Visibility.private?(object) && object.data["actor"] == ap_id do
data |> Map.put("object", object |> Map.get(:data) |> prepare_object) data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
else else
data |> maybe_fix_object_url data |> maybe_fix_object_url

View File

@ -167,7 +167,7 @@ def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) d
with true <- Config.get!([:instance, :federating]), with true <- Config.get!([:instance, :federating]),
true <- type != "Block" || outgoing_blocks, true <- type != "Block" || outgoing_blocks,
false <- Visibility.is_local_public?(activity) do false <- Visibility.local_public?(activity) do
Pleroma.Web.Federator.publish(activity) Pleroma.Web.Federator.publish(activity)
end end
@ -277,7 +277,7 @@ def make_like_data(
object_actor = User.get_cached_by_ap_id(object_actor_id) object_actor = User.get_cached_by_ap_id(object_actor_id)
to = to =
if Visibility.is_public?(object) do if Visibility.public?(object) do
[actor.follower_address, object.data["actor"]] [actor.follower_address, object.data["actor"]]
else else
[object.data["actor"]] [object.data["actor"]]
@ -776,10 +776,9 @@ defp build_flag_object(act) when is_map(act) or is_binary(act) do
build_flag_object(object) build_flag_object(object)
nil -> nil ->
if %Object{} = object = Object.get_by_ap_id(id) do case Object.get_by_ap_id(id) do
build_flag_object(object) %Object{} = object -> build_flag_object(object)
else _ -> %{"id" => id, "deleted" => true}
%{"id" => id, "deleted" => true}
end end
end end
end end

View File

@ -11,28 +11,28 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
require Pleroma.Constants require Pleroma.Constants
@spec is_public?(Object.t() | Activity.t() | map()) :: boolean() @spec public?(Object.t() | Activity.t() | map()) :: boolean()
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false def public?(%Object{data: %{"type" => "Tombstone"}}), do: false
def is_public?(%Object{data: data}), do: is_public?(data) def public?(%Object{data: data}), do: public?(data)
def is_public?(%Activity{data: %{"type" => "Move"}}), do: true def public?(%Activity{data: %{"type" => "Move"}}), do: true
def is_public?(%Activity{data: data}), do: is_public?(data) def public?(%Activity{data: data}), do: public?(data)
def is_public?(%{"directMessage" => true}), do: false def public?(%{"directMessage" => true}), do: false
def is_public?(data) do def public?(data) do
Utils.label_in_message?(Pleroma.Constants.as_public(), data) or Utils.label_in_message?(Pleroma.Constants.as_public(), data) or
Utils.label_in_message?(Utils.as_local_public(), data) Utils.label_in_message?(Utils.as_local_public(), data)
end end
def is_local_public?(%Object{data: data}), do: is_local_public?(data) def local_public?(%Object{data: data}), do: local_public?(data)
def is_local_public?(%Activity{data: data}), do: is_local_public?(data) def local_public?(%Activity{data: data}), do: local_public?(data)
def is_local_public?(data) do def local_public?(data) do
Utils.label_in_message?(Utils.as_local_public(), data) and Utils.label_in_message?(Utils.as_local_public(), data) and
not Utils.label_in_message?(Pleroma.Constants.as_public(), data) not Utils.label_in_message?(Pleroma.Constants.as_public(), data)
end end
def is_private?(activity) do def private?(activity) do
with false <- is_public?(activity), with false <- public?(activity),
%User{follower_address: follower_address} <- %User{follower_address: follower_address} <-
User.get_cached_by_ap_id(activity.data["actor"]) do User.get_cached_by_ap_id(activity.data["actor"]) do
follower_address in activity.data["to"] follower_address in activity.data["to"]
@ -41,20 +41,20 @@ def is_private?(activity) do
end end
end end
def is_announceable?(activity, user, public \\ true) do def announceable?(activity, user, public \\ true) do
is_public?(activity) || public?(activity) ||
(!public && is_private?(activity) && activity.data["actor"] == user.ap_id) (!public && private?(activity) && activity.data["actor"] == user.ap_id)
end end
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true def direct?(%Activity{data: %{"directMessage" => true}}), do: true
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true def direct?(%Object{data: %{"directMessage" => true}}), do: true
def is_direct?(activity) do def direct?(activity) do
!is_public?(activity) && !is_private?(activity) !public?(activity) && !private?(activity)
end end
def is_list?(%{data: %{"listMessage" => _}}), do: true def list?(%{data: %{"listMessage" => _}}), do: true
def is_list?(_), do: false def list?(_), do: false
@spec visible_for_user?(Object.t() | Activity.t() | nil, User.t() | nil) :: boolean() @spec visible_for_user?(Object.t() | Activity.t() | nil, User.t() | nil) :: boolean()
def visible_for_user?(%Object{data: %{"type" => "Tombstone"}}, _), do: false def visible_for_user?(%Object{data: %{"type" => "Tombstone"}}, _), do: false
@ -77,7 +77,7 @@ def visible_for_user?(%{__struct__: module} = message, nil)
when module in [Activity, Object] do when module in [Activity, Object] do
if restrict_unauthenticated_access?(message), if restrict_unauthenticated_access?(message),
do: false, do: false,
else: is_public?(message) and not is_local_public?(message) else: public?(message) and not local_public?(message)
end end
def visible_for_user?(%{__struct__: module} = message, user) def visible_for_user?(%{__struct__: module} = message, user)
@ -86,8 +86,8 @@ def visible_for_user?(%{__struct__: module} = message, user)
y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || []) y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
user_is_local = user.local user_is_local = user.local
federatable = not is_local_public?(message) federatable = not local_public?(message)
(is_public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable) (public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable)
end end
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do

View File

@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
alias Pleroma.ConfigDB alias Pleroma.ConfigDB
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action == :update) plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action == :update)
plug( plug(
@ -76,7 +76,7 @@ def descriptions(conn, _params) do
json(conn, translate_descriptions(descriptions)) json(conn, translate_descriptions(descriptions))
end end
def show(conn, %{only_db: true}) do def show(%{private: %{open_api_spex: %{params: %{only_db: true}}}} = conn, _) do
with :ok <- configurable_from_database() do with :ok <- configurable_from_database() do
configs = Pleroma.Repo.all(ConfigDB) configs = Pleroma.Repo.all(ConfigDB)
@ -128,7 +128,7 @@ def show(conn, _params) do
end end
end end
def update(%{body_params: %{configs: configs}} = conn, _) do def update(%{private: %{open_api_spex: %{body_params: %{configs: configs}}}} = conn, _) do
with :ok <- configurable_from_database() do with :ok <- configurable_from_database() do
results = results =
configs configs

View File

@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
alias Pleroma.Web.Plugs.InstanceStatic alias Pleroma.Web.Plugs.InstanceStatic
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
action_fallback(Pleroma.Web.AdminAPI.FallbackController) action_fallback(Pleroma.Web.AdminAPI.FallbackController)
@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action in [:update, :delete]) plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action in [:update, :delete])
def show(conn, %{name: document_name}) do def show(%{private: %{open_api_spex: %{params: %{name: document_name}}}} = conn, _) do
with {:ok, url} <- InstanceDocument.get(document_name), with {:ok, url} <- InstanceDocument.get(document_name),
{:ok, content} <- File.read(InstanceStatic.file_path(url)) do {:ok, content} <- File.read(InstanceStatic.file_path(url)) do
conn conn
@ -27,13 +27,18 @@ def show(conn, %{name: document_name}) do
end end
end end
def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do def update(
%{
private: %{open_api_spex: %{body_params: %{file: file}, params: %{name: document_name}}}
} = conn,
_
) do
with {:ok, url} <- InstanceDocument.put(document_name, file.path) do with {:ok, url} <- InstanceDocument.put(document_name, file.path) do
json(conn, %{"url" => url}) json(conn, %{"url" => url})
end end
end end
def delete(conn, %{name: document_name}) do def delete(%{private: %{open_api_spex: %{params: %{name: document_name}}}} = conn, _) do
with :ok <- InstanceDocument.delete(document_name) do with :ok <- InstanceDocument.delete(document_name) do
json(conn, %{}) json(conn, %{})
end end

View File

@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
require Logger require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["admin:read:invites"]} when action == :index) plug(OAuthScopesPlug, %{scopes: ["admin:read:invites"]} when action == :index)
plug( plug(
@ -33,14 +33,14 @@ def index(conn, _params) do
end end
@doc "Create an account registration invite token" @doc "Create an account registration invite token"
def create(%{body_params: params} = conn, _) do def create(%{private: %{open_api_spex: %{body_params: params}}} = conn, _) do
{:ok, invite} = UserInviteToken.create_invite(params) {:ok, invite} = UserInviteToken.create_invite(params)
render(conn, "show.json", invite: invite) render(conn, "show.json", invite: invite)
end end
@doc "Revokes invite by token" @doc "Revokes invite by token"
def revoke(%{body_params: %{token: token}} = conn, _) do def revoke(%{private: %{open_api_spex: %{body_params: %{token: token}}}} = conn, _) do
with {:ok, invite} <- UserInviteToken.find_by_token(token), with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
render(conn, "show.json", invite: updated_invite) render(conn, "show.json", invite: updated_invite)
@ -51,7 +51,13 @@ def revoke(%{body_params: %{token: token}} = conn, _) do
end end
@doc "Sends registration invite via email" @doc "Sends registration invite via email"
def email(%{assigns: %{user: user}, body_params: %{email: email} = params} = conn, _) do def email(
%{
assigns: %{user: user},
private: %{open_api_spex: %{body_params: %{email: email} = params}}
} = conn,
_
) do
with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
{_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])}, {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
{:ok, invite_token} <- UserInviteToken.create_invite(), {:ok, invite_token} <- UserInviteToken.create_invite(),

View File

@ -11,7 +11,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
@cachex Pleroma.Config.get([:cachex, :provider], Cachex) @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
@ -27,7 +27,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
defdelegate open_api_operation(action), to: Spec.MediaProxyCacheOperation defdelegate open_api_operation(action), to: Spec.MediaProxyCacheOperation
def index(%{assigns: %{user: _}} = conn, params) do def index(%{assigns: %{user: _}, private: %{open_api_spex: %{params: params}}} = conn, _) do
entries = fetch_entries(params) entries = fetch_entries(params)
urls = paginate_entries(entries, params.page, params.page_size) urls = paginate_entries(entries, params.page, params.page_size)
@ -59,12 +59,19 @@ defp paginate_entries(entries, page, page_size) do
Enum.slice(entries, offset, page_size) Enum.slice(entries, offset, page_size)
end end
def delete(%{assigns: %{user: _}, body_params: %{urls: urls}} = conn, _) do def delete(
%{assigns: %{user: _}, private: %{open_api_spex: %{body_params: %{urls: urls}}}} = conn,
_
) do
MediaProxy.remove_from_banned_urls(urls) MediaProxy.remove_from_banned_urls(urls)
json(conn, %{}) json(conn, %{})
end end
def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: ban}} = conn, _) do def purge(
%{assigns: %{user: _}, private: %{open_api_spex: %{body_params: %{urls: urls, ban: ban}}}} =
conn,
_
) do
MediaProxy.Invalidation.purge(urls) MediaProxy.Invalidation.purge(urls)
if ban do if ban do

View File

@ -11,7 +11,7 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
require Logger require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
@ -31,7 +31,13 @@ def index(conn, _params) do
end end
end end
def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do def follow(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{relay_url: target}}}
} = conn,
_
) do
with {:ok, _message} <- Relay.follow(target) do with {:ok, _message} <- Relay.follow(target) do
ModerationLog.insert_log(%{action: "relay_follow", actor: admin, target: target}) ModerationLog.insert_log(%{action: "relay_follow", actor: admin, target: target})
@ -44,7 +50,13 @@ def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn,
end end
end end
def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target} = params} = conn, _) do def unfollow(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{relay_url: target} = params}}
} = conn,
_
) do
with {:ok, _message} <- Relay.unfollow(target, %{force: params[:force]}) do with {:ok, _message} <- Relay.unfollow(target, %{force: params[:force]}) do
ModerationLog.insert_log(%{action: "relay_unfollow", actor: admin, target: target}) ModerationLog.insert_log(%{action: "relay_unfollow", actor: admin, target: target})

View File

@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
require Logger require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["admin:read:reports"]} when action in [:index, :show]) plug(OAuthScopesPlug, %{scopes: ["admin:read:reports"]} when action in [:index, :show])
plug( plug(
@ -31,13 +31,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ReportOperation defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ReportOperation
def index(conn, params) do def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
reports = Utils.get_reports(params, params.page, params.page_size) reports = Utils.get_reports(params, params.page, params.page_size)
render(conn, "index.json", reports: reports) render(conn, "index.json", reports: reports)
end end
def show(conn, %{id: id}) do def show(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Activity{} = report <- Activity.get_report(id) do with %Activity{} = report <- Activity.get_report(id) do
render(conn, "show.json", Report.extract_report_info(report)) render(conn, "show.json", Report.extract_report_info(report))
else else
@ -45,7 +45,13 @@ def show(conn, %{id: id}) do
end end
end end
def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn, _) do def update(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{reports: reports}}}
} = conn,
_
) do
result = result =
Enum.map(reports, fn report -> Enum.map(reports, fn report ->
case CommonAPI.update_report_state(report.id, report.state) do case CommonAPI.update_report_state(report.id, report.state) do
@ -73,9 +79,13 @@ def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn,
end end
end end
def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = conn, %{ def notes_create(
id: report_id %{
}) do assigns: %{user: user},
private: %{open_api_spex: %{body_params: %{content: content}, params: %{id: report_id}}}
} = conn,
_
) do
with {:ok, _} <- ReportNote.create(user.id, report_id, content), with {:ok, _} <- ReportNote.create(user.id, report_id, content),
report <- Activity.get_by_id_with_user_actor(report_id) do report <- Activity.get_by_id_with_user_actor(report_id) do
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
@ -92,10 +102,20 @@ def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = c
end end
end end
def notes_delete(%{assigns: %{user: user}} = conn, %{ def notes_delete(
id: note_id, %{
report_id: report_id assigns: %{user: user},
}) do private: %{
open_api_spex: %{
params: %{
id: note_id,
report_id: report_id
}
}
}
} = conn,
_
) do
with {:ok, note} <- ReportNote.destroy(note_id), with {:ok, note} <- ReportNote.destroy(note_id),
report <- Activity.get_by_id_with_user_actor(report_id) do report <- Activity.get_by_id_with_user_actor(report_id) do
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{

View File

@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.UserController do
@users_page_size 50 @users_page_size 50
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
@ -51,13 +51,22 @@ defmodule Pleroma.Web.AdminAPI.UserController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.UserOperation defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.UserOperation
def delete(conn, %{nickname: nickname}) do def delete(%{private: %{open_api_spex: %{params: %{nickname: nickname}}}} = conn, _) do
conn conn
|> Map.put(:body_params, %{nicknames: [nickname]}) |> do_deletes([nickname])
|> delete(%{})
end end
def delete(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do def delete(
%{
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
} = conn,
_
) do
conn
|> do_deletes(nicknames)
end
defp do_deletes(%{assigns: %{user: admin}} = conn, nicknames) when is_list(nicknames) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1) users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
Enum.each(users, fn user -> Enum.each(users, fn user ->
@ -77,9 +86,13 @@ def delete(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = co
def follow( def follow(
%{ %{
assigns: %{user: admin}, assigns: %{user: admin},
body_params: %{ private: %{
follower: follower_nick, open_api_spex: %{
followed: followed_nick body_params: %{
follower: follower_nick,
followed: followed_nick
}
}
} }
} = conn, } = conn,
_ _
@ -102,9 +115,13 @@ def follow(
def unfollow( def unfollow(
%{ %{
assigns: %{user: admin}, assigns: %{user: admin},
body_params: %{ private: %{
follower: follower_nick, open_api_spex: %{
followed: followed_nick body_params: %{
follower: follower_nick,
followed: followed_nick
}
}
} }
} = conn, } = conn,
_ _
@ -124,7 +141,13 @@ def unfollow(
json(conn, "ok") json(conn, "ok")
end end
def create(%{assigns: %{user: admin}, body_params: %{users: users}} = conn, _) do def create(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{users: users}}}
} = conn,
_
) do
changesets = changesets =
users users
|> Enum.map(fn %{nickname: nickname, email: email, password: password} -> |> Enum.map(fn %{nickname: nickname, email: email, password: password} ->
@ -178,7 +201,13 @@ def create(%{assigns: %{user: admin}, body_params: %{users: users}} = conn, _) d
end end
end end
def show(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do def show(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{params: %{nickname: nickname}}}
} = conn,
_
) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
render(conn, "show.json", %{user: user}) render(conn, "show.json", %{user: user})
else else
@ -186,7 +215,11 @@ def show(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do
end end
end end
def toggle_activation(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do def toggle_activation(
%{assigns: %{user: admin}, private: %{open_api_spex: %{params: %{nickname: nickname}}}} =
conn,
_
) do
user = User.get_cached_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.set_activation(user, !user.is_active) {:ok, updated_user} = User.set_activation(user, !user.is_active)
@ -202,7 +235,13 @@ def toggle_activation(%{assigns: %{user: admin}} = conn, %{nickname: nickname})
render(conn, "show.json", user: updated_user) render(conn, "show.json", user: updated_user)
end end
def activate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do def activate(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
} = conn,
_
) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1) users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_activation(users, true) {:ok, updated_users} = User.set_activation(users, true)
@ -212,10 +251,16 @@ def activate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} =
action: "activate" action: "activate"
}) })
render(conn, "index.json", users: Keyword.values(updated_users)) render(conn, "index.json", users: updated_users)
end end
def deactivate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do def deactivate(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
} = conn,
_
) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1) users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_activation(users, false) {:ok, updated_users} = User.set_activation(users, false)
@ -225,10 +270,16 @@ def deactivate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}}
action: "deactivate" action: "deactivate"
}) })
render(conn, "index.json", users: Keyword.values(updated_users)) render(conn, "index.json", users: updated_users)
end end
def approve(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do def approve(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
} = conn,
_
) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1) users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.approve(users) {:ok, updated_users} = User.approve(users)
@ -241,7 +292,13 @@ def approve(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = c
render(conn, "index.json", users: updated_users) render(conn, "index.json", users: updated_users)
end end
def suggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do def suggest(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
} = conn,
_
) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1) users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_suggestion(users, true) {:ok, updated_users} = User.set_suggestion(users, true)
@ -254,7 +311,13 @@ def suggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = c
render(conn, "index.json", users: updated_users) render(conn, "index.json", users: updated_users)
end end
def unsuggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do def unsuggest(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
} = conn,
_
) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1) users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_suggestion(users, false) {:ok, updated_users} = User.set_suggestion(users, false)
@ -267,7 +330,7 @@ def unsuggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} =
render(conn, "index.json", users: updated_users) render(conn, "index.json", users: updated_users)
end end
def index(conn, params) do def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
filters = maybe_parse_filters(params[:filters]) filters = maybe_parse_filters(params[:filters])

View File

@ -27,10 +27,12 @@ def init(opts) do
@impl Plug @impl Plug
def call(conn, %{operation_id: operation_id, render_error: render_error}) do def call(conn, %{operation_id: operation_id, render_error: render_error} = opts) do
{spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn) {spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)
operation = operation_lookup[operation_id] operation = operation_lookup[operation_id]
cast_opts = opts |> Map.take([:replace_params]) |> Map.to_list()
content_type = content_type =
case Conn.get_req_header(conn, "content-type") do case Conn.get_req_header(conn, "content-type") do
[header_value | _] -> [header_value | _] ->
@ -44,7 +46,7 @@ def call(conn, %{operation_id: operation_id, render_error: render_error}) do
conn = Conn.put_private(conn, :operation_id, operation_id) conn = Conn.put_private(conn, :operation_id, operation_id)
case cast_and_validate(spec, operation, conn, content_type, strict?()) do case cast_and_validate(spec, operation, conn, content_type, strict?(), cast_opts) do
{:ok, conn} -> {:ok, conn} ->
conn conn
@ -94,11 +96,11 @@ def call(
def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts) def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do defp cast_and_validate(spec, operation, conn, content_type, true = _strict, cast_opts) do
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
end end
defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do defp cast_and_validate(spec, operation, conn, content_type, false = _strict, cast_opts) do
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
{:ok, conn} -> {:ok, conn} ->
{:ok, conn} {:ok, conn}
@ -123,7 +125,7 @@ defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
end) end)
conn = %Conn{conn | query_params: query_params} conn = %Conn{conn | query_params: query_params}
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
end end
end end

View File

@ -62,7 +62,7 @@ def with_relationships_param do
Operation.parameter( Operation.parameter(
:with_relationships, :with_relationships,
:query, :query,
BooleanLike, BooleanLike.schema(),
"Embed relationships into accounts. **If this parameter is not set account's `pleroma.relationship` is going to be `null`.**" "Embed relationships into accounts. **If this parameter is not set account's `pleroma.relationship` is going to be `null`.**"
) )
end end

Some files were not shown because too many files have changed in this diff Show More