Merge branch 'develop' into audit-mix-audit-report
This commit is contained in:
commit
45459c8398
156 changed files with 8723 additions and 731 deletions
|
|
@ -62,7 +62,8 @@ defmodule Mix.Pleroma do
|
|||
{Majic.Pool,
|
||||
[name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]}
|
||||
] ++
|
||||
http_children(adapter)
|
||||
http_children(adapter) ++
|
||||
search_children()
|
||||
|
||||
cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
|
||||
|
||||
|
|
@ -141,4 +142,14 @@ defmodule Mix.Pleroma do
|
|||
end
|
||||
|
||||
defp http_children(_), do: []
|
||||
|
||||
defp search_children do
|
||||
case Pleroma.Config.get([Pleroma.Search, :module]) do
|
||||
Pleroma.Search.ParadeDB ->
|
||||
[Pleroma.Search.ParadeDB.Repo]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -335,9 +335,12 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
group
|
||||
|> Pleroma.Config.Loader.filter_group(settings)
|
||||
|> Enum.each(fn {key, value} ->
|
||||
{:ok, _} = ConfigDB.update_or_create(%{group: group, key: key, value: value})
|
||||
|
||||
shell_info("Settings for key #{key} migrated.")
|
||||
if ConfigDB.static?(group, key) do
|
||||
shell_info("Settings for key #{key} must remain in static configuration; skipping.")
|
||||
else
|
||||
{:ok, _} = ConfigDB.update_or_create(%{group: group, key: key, value: value})
|
||||
shell_info("Settings for key #{key} migrated.")
|
||||
end
|
||||
end)
|
||||
|
||||
shell_info("Settings for group #{inspect(group)} migrated.")
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@
|
|||
|
||||
defmodule Mix.Tasks.Pleroma.Search.Indexer do
|
||||
import Mix.Pleroma
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Workers.SearchIndexingWorker
|
||||
|
||||
def run(["create_index"]) do
|
||||
start_pleroma()
|
||||
|
|
@ -35,49 +32,30 @@ defmodule Mix.Tasks.Pleroma.Search.Indexer do
|
|||
strict: [
|
||||
chunk: :integer,
|
||||
limit: :integer,
|
||||
step: :integer
|
||||
step: :integer,
|
||||
before: :string
|
||||
]
|
||||
)
|
||||
|
||||
start_pleroma()
|
||||
|
||||
chunk_size = Keyword.get(options, :chunk, 100)
|
||||
limit = Keyword.get(options, :limit, 100_000)
|
||||
per_step = Keyword.get(options, :step, 1000)
|
||||
result =
|
||||
Pleroma.Search.Backfill.run(
|
||||
options ++
|
||||
[
|
||||
on_page: fn page ->
|
||||
IO.puts(
|
||||
"Queued #{page.count} activities (#{page.enqueued} total), " <>
|
||||
"checkpoint: #{page.next_cursor}"
|
||||
)
|
||||
end
|
||||
]
|
||||
)
|
||||
|
||||
chunks = max(div(limit, per_step), 1)
|
||||
IO.puts("Queued #{result.enqueued} activities")
|
||||
|
||||
1..chunks
|
||||
|> Enum.each(fn step ->
|
||||
q =
|
||||
from(a in Pleroma.Activity,
|
||||
limit: ^per_step,
|
||||
offset: ^per_step * (^step - 1),
|
||||
select: [:id],
|
||||
order_by: [desc: :id]
|
||||
)
|
||||
|
||||
{:ok, ids} =
|
||||
Pleroma.Repo.transaction(fn ->
|
||||
Pleroma.Repo.stream(q, timeout: :infinity)
|
||||
|> Enum.map(fn a ->
|
||||
a.id
|
||||
end)
|
||||
end)
|
||||
|
||||
IO.puts("Got #{length(ids)} activities, adding to indexer")
|
||||
|
||||
ids
|
||||
|> Enum.chunk_every(chunk_size)
|
||||
|> Enum.each(fn chunk ->
|
||||
IO.puts("Adding #{length(chunk)} activities to indexing queue")
|
||||
|
||||
chunk
|
||||
|> Enum.map(fn id ->
|
||||
SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => id})
|
||||
end)
|
||||
|> Oban.insert_all()
|
||||
end)
|
||||
end)
|
||||
unless result.exhausted do
|
||||
IO.puts("Resume with --before #{result.next_cursor}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -81,12 +81,15 @@ defmodule Pleroma.Application do
|
|||
[
|
||||
Pleroma.PromEx,
|
||||
Pleroma.LDAP,
|
||||
Pleroma.Repo,
|
||||
Config.TransferTask,
|
||||
Pleroma.Emoji,
|
||||
Pleroma.Web.Plugs.RateLimiter.Supervisor,
|
||||
{Task.Supervisor, name: Pleroma.TaskSupervisor}
|
||||
Pleroma.Repo
|
||||
] ++
|
||||
search_children() ++
|
||||
[
|
||||
Config.TransferTask,
|
||||
Pleroma.Emoji,
|
||||
Pleroma.Web.Plugs.RateLimiter.Supervisor,
|
||||
{Task.Supervisor, name: Pleroma.TaskSupervisor}
|
||||
] ++
|
||||
cachex_children() ++
|
||||
http_children(adapter) ++
|
||||
[
|
||||
|
|
@ -198,6 +201,16 @@ defmodule Pleroma.Application do
|
|||
]
|
||||
end
|
||||
|
||||
defp search_children do
|
||||
case Config.get([Pleroma.Search, :module]) do
|
||||
Pleroma.Search.ParadeDB ->
|
||||
[Pleroma.Search.ParadeDB.Repo]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp emoji_packs_expiration,
|
||||
do: expiration(default: :timer.seconds(5 * 60), interval: :timer.seconds(60))
|
||||
|
||||
|
|
@ -317,7 +330,8 @@ defmodule Pleroma.Application do
|
|||
config = Config.get(ConcurrentLimiter, [])
|
||||
|
||||
[
|
||||
Pleroma.Search
|
||||
Pleroma.Search,
|
||||
Pleroma.Webhook.Notify
|
||||
]
|
||||
|> Enum.each(fn module ->
|
||||
mod_config = Keyword.get(config, module, [])
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ defmodule Pleroma.Config.TransferTask do
|
|||
|
||||
settings =
|
||||
(Repo.all(ConfigDB) ++ deleted_settings)
|
||||
|> Enum.reject(&ConfigDB.static?/1)
|
||||
|> Enum.map(&merge_with_default/1)
|
||||
|
||||
started_applications = Application.started_applications()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ defmodule Pleroma.ConfigDB do
|
|||
{:pleroma, :mrf_keyword, :replace}
|
||||
]
|
||||
|
||||
@static_config [
|
||||
{:pleroma, Pleroma.Search},
|
||||
{:pleroma, Pleroma.Search.ParadeDB},
|
||||
{:pleroma, Pleroma.Search.ParadeDB.Repo}
|
||||
]
|
||||
|
||||
schema "config" do
|
||||
field(:key, Pleroma.EctoType.Config.Atom)
|
||||
field(:group, Pleroma.EctoType.Config.Atom)
|
||||
|
|
@ -32,16 +38,32 @@ defmodule Pleroma.ConfigDB do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
@spec get_all_as_keyword() :: keyword()
|
||||
def get_all_as_keyword do
|
||||
@spec get_all_as_keyword(keyword()) :: keyword()
|
||||
def get_all_as_keyword(options \\ []) do
|
||||
ConfigDB
|
||||
|> select([c], {c.group, c.key, c.value})
|
||||
|> Repo.all()
|
||||
|> maybe_reject_static(options)
|
||||
|> Enum.reduce([], fn {group, key, value}, acc ->
|
||||
Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
|
||||
end)
|
||||
end
|
||||
|
||||
def static?(%{group: group, key: key}), do: static?(group, key)
|
||||
def static?(_config), do: false
|
||||
|
||||
def static?(group, key) do
|
||||
Enum.any?(@static_config, fn {static_group, static_key} ->
|
||||
group in [static_group, inspect(static_group)] and key in [static_key, inspect(static_key)]
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_reject_static(configs, exclude_static: true) do
|
||||
Enum.reject(configs, fn {group, key, _value} -> static?(group, key) end)
|
||||
end
|
||||
|
||||
defp maybe_reject_static(configs, _options), do: configs
|
||||
|
||||
@spec get_all_by_group(atom() | String.t()) :: [t()]
|
||||
def get_all_by_group(group) do
|
||||
from(c in ConfigDB, where: c.group == ^group) |> Repo.all()
|
||||
|
|
|
|||
|
|
@ -346,6 +346,16 @@ defmodule Pleroma.ModerationLog do
|
|||
"@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
|
||||
end
|
||||
|
||||
def get_log_entry_message(%ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "reject",
|
||||
"subject" => users
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} rejected users: #{users_to_nicknames_string(users)}"
|
||||
end
|
||||
|
||||
def get_log_entry_message(%ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
|
|
|
|||
|
|
@ -15,12 +15,28 @@ defmodule Pleroma.Object.Containment do
|
|||
actor
|
||||
end
|
||||
|
||||
def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
|
||||
get_actor(%{"actor" => actor})
|
||||
end
|
||||
|
||||
def get_actor(%{"actor" => nil}), do: nil
|
||||
|
||||
def get_actor(%{"actor" => []}), do: nil
|
||||
|
||||
def get_actor(%{"actor" => actor}) when is_list(actor) do
|
||||
if is_binary(Enum.at(actor, 0)) do
|
||||
Enum.at(actor, 0)
|
||||
else
|
||||
Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|
||||
|> Map.get("id")
|
||||
case actor do
|
||||
[first | _] when is_binary(first) ->
|
||||
first
|
||||
|
||||
_ ->
|
||||
Enum.find_value(actor, fn
|
||||
%{"type" => type, "id" => id}
|
||||
when type in ["Person", "Service", "Application"] and is_binary(id) ->
|
||||
id
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -28,10 +44,6 @@ defmodule Pleroma.Object.Containment do
|
|||
id
|
||||
end
|
||||
|
||||
def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
|
||||
get_actor(%{"actor" => actor})
|
||||
end
|
||||
|
||||
def get_object(%{"object" => id}) when is_binary(id) do
|
||||
id
|
||||
end
|
||||
|
|
@ -67,9 +79,11 @@ defmodule Pleroma.Object.Containment do
|
|||
|
||||
def contain_origin(id, %{"actor" => _actor} = params) do
|
||||
id_uri = URI.parse(id)
|
||||
actor_uri = URI.parse(get_actor(params))
|
||||
|
||||
compare_uris(actor_uri, id_uri)
|
||||
case get_actor(params) do
|
||||
actor when is_binary(actor) -> compare_uris(URI.parse(actor), id_uri)
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def contain_origin(id, %{"attributedTo" => actor} = params),
|
||||
|
|
|
|||
|
|
@ -12,18 +12,27 @@ defmodule Pleroma.ReverseProxy do
|
|||
@keep_resp_headers @resp_cache_headers ++
|
||||
~w(content-length content-type content-disposition content-encoding) ++
|
||||
~w(content-range accept-ranges vary)
|
||||
@default_cache_control_header "public, max-age=1209600, immutable"
|
||||
@default_cache_control_header "public, max-age=1209600, immutable, no-transform"
|
||||
@valid_resp_codes [200, 206, 304]
|
||||
@max_read_duration :timer.seconds(30)
|
||||
@max_body_length :infinity
|
||||
@failed_request_ttl :timer.seconds(60)
|
||||
@sniff_bytes 8 * 1024
|
||||
@methods ~w(GET HEAD)
|
||||
|
||||
@allowed_mime_types Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types], [])
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
defmodule StreamError do
|
||||
@moduledoc false
|
||||
defexception [:url, :reason]
|
||||
|
||||
@impl true
|
||||
def message(%{url: url, reason: reason}) do
|
||||
"reverse proxy stream from #{url} failed: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
def max_read_duration_default, do: @max_read_duration
|
||||
def default_cache_control_header, do: @default_cache_control_header
|
||||
|
||||
|
|
@ -60,12 +69,7 @@ defmodule Pleroma.ReverseProxy do
|
|||
* `inline_content_types`:
|
||||
* `true` will not alter `content-disposition` (up to the upstream),
|
||||
* `false` will add `content-disposition: attachment` to any request,
|
||||
* a list of whitelisted content types for which `content-disposition: inline`
|
||||
is always set (overriding any upstream header) so the media can be embedded
|
||||
in pages; the filename is derived from the content type
|
||||
|
||||
* `sniff_content_type` (default `false`): detects an image MIME type from the first
|
||||
response chunk when the upstream type is missing or `application/octet-stream`.
|
||||
* a list of whitelisted content types
|
||||
|
||||
* `req_headers`, `resp_headers` additional headers.
|
||||
|
||||
|
|
@ -102,7 +106,6 @@ defmodule Pleroma.ReverseProxy do
|
|||
| {:req_headers, [{String.t(), String.t()}]}
|
||||
| {:resp_headers, [{String.t(), String.t()}]}
|
||||
| {:inline_content_types, boolean() | list(String.t())}
|
||||
| {:sniff_content_type, boolean()}
|
||||
| {:redirect_on_failure, boolean()}
|
||||
|
||||
@spec call(Plug.Conn.t(), String.t(), list(option())) :: Plug.Conn.t()
|
||||
|
|
@ -196,151 +199,93 @@ defmodule Pleroma.ReverseProxy do
|
|||
end
|
||||
|
||||
defp request_with_constraints(method, url, headers, client_opts, opts) do
|
||||
case request(method, url, headers, client_opts) do
|
||||
{:ok, _code, headers, client} = response ->
|
||||
case header_length_constraint(
|
||||
headers,
|
||||
Keyword.get(opts, :max_body_length, @max_body_length)
|
||||
) do
|
||||
:ok ->
|
||||
response
|
||||
method
|
||||
|> request(url, headers, client_opts)
|
||||
|> constrain_response(Keyword.get(opts, :max_body_length, @max_body_length))
|
||||
end
|
||||
|
||||
error ->
|
||||
client().close(client)
|
||||
error
|
||||
end
|
||||
defp constrain_response({:ok, code, headers, client}, limit) do
|
||||
case header_length_constraint(headers, limit) do
|
||||
{:ok, headers} ->
|
||||
{:ok, code, headers, client}
|
||||
|
||||
response ->
|
||||
response
|
||||
error ->
|
||||
client().close(client)
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp constrain_response({:ok, code, headers}, limit) do
|
||||
case header_length_constraint(headers, limit) do
|
||||
{:ok, headers} -> {:ok, code, headers}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp constrain_response(response, _limit), do: response
|
||||
|
||||
defp response(conn, client, url, status, headers, opts) do
|
||||
Logger.debug("#{__MODULE__} #{status} #{url} #{inspect(headers)}")
|
||||
|
||||
{headers, client, prefetched} = maybe_prefetch_for_content_type(headers, client, opts)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> put_resp_headers(build_resp_headers(headers, opts))
|
||||
|> streaming_compat
|
||||
|> send_chunked(status)
|
||||
|> chunk_reply(client, opts, prefetched)
|
||||
|> chunk_reply(client, opts)
|
||||
|
||||
case result do
|
||||
{:ok, conn} ->
|
||||
halt(conn)
|
||||
|
||||
{:error, :closed, conn} ->
|
||||
{:error, {:downstream, _error}, conn} ->
|
||||
client().close(client)
|
||||
halt(conn)
|
||||
|
||||
{:error, error, conn} ->
|
||||
{:error, {:upstream, error}, conn} ->
|
||||
Logger.warning(
|
||||
"#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}"
|
||||
)
|
||||
|
||||
client().close(client)
|
||||
halt(conn)
|
||||
track_failed_url(url, error, opts)
|
||||
raise_stream_error(conn, url, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp chunk_reply(conn, client, opts, :none) do
|
||||
defp chunk_reply(conn, client, opts) do
|
||||
chunk_reply(conn, client, opts, 0, 0)
|
||||
end
|
||||
|
||||
defp chunk_reply(conn, _client, _opts, :done), do: {:ok, conn}
|
||||
defp chunk_reply(conn, _client, _opts, {:error, error}), do: {:error, error, conn}
|
||||
|
||||
defp chunk_reply(conn, client, opts, {:ok, data, duration}) do
|
||||
with :ok <-
|
||||
body_size_constraint(
|
||||
byte_size(data),
|
||||
Keyword.get(opts, :max_body_length, @max_body_length)
|
||||
),
|
||||
{:ok, conn} <- chunk(conn, data) do
|
||||
chunk_reply(conn, client, opts, byte_size(data), duration)
|
||||
else
|
||||
{:error, error} -> {:error, error, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp chunk_reply(conn, client, opts, sent_so_far, duration) do
|
||||
with {:ok, data, client, duration} <- read_chunk(client, duration, opts),
|
||||
with {:ok, duration} <-
|
||||
check_read_duration(
|
||||
duration,
|
||||
Keyword.get(opts, :max_read_duration, @max_read_duration)
|
||||
),
|
||||
{:ok, data, client} <- client().stream_body(client),
|
||||
{:ok, duration} <- increase_read_duration(duration),
|
||||
sent_so_far = sent_so_far + byte_size(data),
|
||||
:ok <-
|
||||
body_size_constraint(
|
||||
sent_so_far,
|
||||
Keyword.get(opts, :max_body_length, @max_body_length)
|
||||
),
|
||||
{:ok, conn} <- chunk(conn, data) do
|
||||
{:ok, conn} <- write_chunk(conn, data) do
|
||||
chunk_reply(conn, client, opts, sent_so_far, duration)
|
||||
else
|
||||
:done -> {:ok, conn}
|
||||
{:error, error} -> {:error, error, conn}
|
||||
{:error, {source, error}} -> {:error, {source, error}, conn}
|
||||
{:error, error} -> {:error, {:upstream, error}, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp read_chunk(client, duration, opts) do
|
||||
with {:ok, timer} <-
|
||||
check_read_duration(
|
||||
duration,
|
||||
Keyword.get(opts, :max_read_duration, @max_read_duration)
|
||||
),
|
||||
{:ok, data, client} <- client().stream_body(client),
|
||||
{:ok, duration} <- increase_read_duration(timer) do
|
||||
{:ok, data, client, duration}
|
||||
defp write_chunk(conn, data) do
|
||||
case chunk(conn, data) do
|
||||
{:error, error} -> {:error, {:downstream, error}}
|
||||
result -> result
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_prefetch_for_content_type(headers, client, opts) do
|
||||
if Keyword.get(opts, :sniff_content_type, false) and generic_content_type?(headers) do
|
||||
case read_chunk(client, 0, opts) do
|
||||
{:ok, data, client, duration} ->
|
||||
{maybe_put_image_content_type(headers, data), client, {:ok, data, duration}}
|
||||
|
||||
:done ->
|
||||
{headers, client, :done}
|
||||
|
||||
{:error, error} ->
|
||||
{headers, client, {:error, error}}
|
||||
end
|
||||
else
|
||||
{headers, client, :none}
|
||||
end
|
||||
end
|
||||
|
||||
defp generic_content_type?(headers) do
|
||||
headers
|
||||
|> get_content_type()
|
||||
|> String.trim()
|
||||
|> String.downcase()
|
||||
|> then(&(&1 in ["", "application/octet-stream"]))
|
||||
end
|
||||
|
||||
defp maybe_put_image_content_type(headers, data) do
|
||||
with false <- data == "",
|
||||
{:ok, %{mime_type: "image/" <> _ = content_type}} <-
|
||||
Majic.perform({:bytes, binary_part(data, 0, min(byte_size(data), @sniff_bytes))},
|
||||
pool: Pleroma.MajicPool
|
||||
) do
|
||||
[
|
||||
{"content-type", content_type}
|
||||
| Enum.reject(headers, fn {key, _} -> key == "content-type" end)
|
||||
]
|
||||
else
|
||||
_ -> headers
|
||||
end
|
||||
rescue
|
||||
error ->
|
||||
Logger.debug("#{__MODULE__}: content-type sniffing failed: #{Exception.message(error)}")
|
||||
headers
|
||||
catch
|
||||
kind, reason ->
|
||||
Logger.debug("#{__MODULE__}: content-type sniffing failed: #{kind}: #{inspect(reason)}")
|
||||
headers
|
||||
end
|
||||
|
||||
defp head_response(conn, url, code, headers, opts) do
|
||||
Logger.debug("#{__MODULE__} #{code} #{url} #{inspect(headers)}")
|
||||
|
||||
|
|
@ -388,33 +333,16 @@ defmodule Pleroma.ReverseProxy do
|
|||
|> build_req_range_or_encoding_header(opts)
|
||||
|> build_req_user_agent_header(opts)
|
||||
|> merge_headers(Keyword.get(opts, :req_headers, []))
|
||||
|> maybe_force_identity_encoding(opts)
|
||||
end
|
||||
|
||||
defp merge_headers(headers, extra_headers) do
|
||||
Enum.reduce(extra_headers, headers, fn {key, value}, headers ->
|
||||
List.keystore(headers, String.downcase(key), 0, {String.downcase(key), value})
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_force_identity_encoding(headers, opts) do
|
||||
if Keyword.get(opts, :sniff_content_type, false) do
|
||||
List.keystore(headers, "accept-encoding", 0, {"accept-encoding", "identity"})
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
# Disable content-encoding if any @range_headers are requested (see #1823).
|
||||
defp build_req_range_or_encoding_header(headers, _opts) do
|
||||
range? = Enum.any?(headers, fn {header, _} -> Enum.member?(@range_headers, header) end)
|
||||
|
||||
cond do
|
||||
range? && List.keymember?(headers, "accept-encoding", 0) ->
|
||||
List.keydelete(headers, "accept-encoding", 0)
|
||||
|
||||
true ->
|
||||
headers
|
||||
if range? && List.keymember?(headers, "accept-encoding", 0) do
|
||||
List.keydelete(headers, "accept-encoding", 0)
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -433,7 +361,30 @@ defmodule Pleroma.ReverseProxy do
|
|||
|> build_resp_cache_headers(opts)
|
||||
|> sanitise_content_type()
|
||||
|> build_resp_content_disposition_header(opts)
|
||||
|> Keyword.merge(Keyword.get(opts, :resp_headers, []))
|
||||
|> merge_headers(Keyword.get(opts, :resp_headers, []))
|
||||
|> ensure_no_transform()
|
||||
end
|
||||
|
||||
defp merge_headers(headers, extra_headers) do
|
||||
Enum.reduce(extra_headers, headers, fn {key, value}, headers ->
|
||||
List.keystore(headers, String.downcase(key), 0, {String.downcase(key), value})
|
||||
end)
|
||||
end
|
||||
|
||||
defp ensure_no_transform(headers) do
|
||||
{_, cache_control} =
|
||||
List.keyfind(headers, "cache-control", 0, {"cache-control", @default_cache_control_header})
|
||||
|
||||
directives =
|
||||
cache_control
|
||||
|> String.split(",")
|
||||
|> Enum.map(&(&1 |> String.trim() |> String.downcase()))
|
||||
|
||||
if "no-transform" in directives do
|
||||
headers
|
||||
else
|
||||
replace_header(headers, "cache-control", cache_control <> ", no-transform")
|
||||
end
|
||||
end
|
||||
|
||||
defp sanitise_content_type(headers) do
|
||||
|
|
@ -509,20 +460,7 @@ defmodule Pleroma.ReverseProxy do
|
|||
|
||||
replace_header(headers, "content-disposition", disposition)
|
||||
else
|
||||
if opt == true do
|
||||
headers
|
||||
else
|
||||
name = inline_filename(content_type)
|
||||
|
||||
disposition =
|
||||
if name do
|
||||
"inline; filename=\"#{name}\""
|
||||
else
|
||||
"inline"
|
||||
end
|
||||
|
||||
replace_header(headers, "content-disposition", disposition)
|
||||
end
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -530,28 +468,34 @@ defmodule Pleroma.ReverseProxy do
|
|||
[{key, value} | Enum.reject(headers, fn {header, _} -> header == key end)]
|
||||
end
|
||||
|
||||
defp inline_filename(content_type) do
|
||||
case MIME.extensions(content_type) do
|
||||
[ext | _] when ext != "" -> "inline.#{ext}"
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
defp header_length_constraint(headers, limit) do
|
||||
lengths =
|
||||
for {"content-length", value} <- headers,
|
||||
do: parse_content_length(value)
|
||||
|
||||
defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do
|
||||
with {_, size} <- List.keyfind(headers, "content-length", 0),
|
||||
{size, _} <- Integer.parse(size),
|
||||
true <- size <= limit do
|
||||
:ok
|
||||
else
|
||||
false ->
|
||||
{:error, :body_too_large}
|
||||
case Enum.uniq(lengths) do
|
||||
[] ->
|
||||
{:ok, headers}
|
||||
|
||||
[size] when is_integer(size) ->
|
||||
case body_size_constraint(size, limit) do
|
||||
:ok -> {:ok, replace_header(headers, "content-length", to_string(size))}
|
||||
error -> error
|
||||
end
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
{:error, :invalid_content_length}
|
||||
end
|
||||
end
|
||||
|
||||
defp header_length_constraint(_, _), do: :ok
|
||||
defp parse_content_length(value) when is_binary(value) do
|
||||
case Integer.parse(value) do
|
||||
{size, ""} when size >= 0 -> size
|
||||
_ -> :invalid
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_content_length(_value), do: :invalid
|
||||
|
||||
defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size > limit do
|
||||
{:error, :body_too_large}
|
||||
|
|
@ -587,6 +531,40 @@ defmodule Pleroma.ReverseProxy do
|
|||
|
||||
defp client, do: Pleroma.ReverseProxy.Client.Wrapper
|
||||
|
||||
# Neither Plug adapter exposes a public way to abort a committed response.
|
||||
# Cowboy finalizes chunked HTTP/1 responses when the request process exits,
|
||||
# so terminate the connection first to leave the response visibly incomplete.
|
||||
defp raise_stream_error(
|
||||
%Plug.Conn{adapter: {Plug.Cowboy.Conn, %{pid: connection_pid}}} = conn,
|
||||
url,
|
||||
error
|
||||
) do
|
||||
if Plug.Conn.get_http_protocol(conn) in [:"HTTP/1.0", :"HTTP/1.1"] do
|
||||
Process.exit(connection_pid, :kill)
|
||||
end
|
||||
|
||||
raise StreamError, url: url, reason: error
|
||||
end
|
||||
|
||||
# Bandit turns this exception into RST_STREAM; a regular exception sends an
|
||||
# empty DATA frame with END_STREAM and makes the partial body look complete.
|
||||
defp raise_stream_error(
|
||||
%Plug.Conn{
|
||||
adapter: {Bandit.Adapter, %{transport: %{stream_id: stream_id}}}
|
||||
},
|
||||
url,
|
||||
error
|
||||
) do
|
||||
raise Bandit.HTTP2.Errors.StreamError,
|
||||
message: StreamError.message(%StreamError{url: url, reason: error}),
|
||||
error_code: Bandit.HTTP2.Errors.internal_error(),
|
||||
stream_id: stream_id
|
||||
end
|
||||
|
||||
defp raise_stream_error(_conn, url, error) do
|
||||
raise StreamError, url: url, reason: error
|
||||
end
|
||||
|
||||
defp track_failed_url(url, error, opts) do
|
||||
ttl =
|
||||
unless error in [:body_too_large, 400, 204] do
|
||||
|
|
@ -598,19 +576,6 @@ defmodule Pleroma.ReverseProxy do
|
|||
@cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl)
|
||||
end
|
||||
|
||||
# When Cowboy handles a chunked response with a content-length header it streams
|
||||
# over HTTP 1.1 instead of chunking. Bandit cannot stream over HTTP 1.1 so the header
|
||||
# must be stripped or it breaks RFC compliance for Transfer Encoding: Chunked. RFC9112§6.2
|
||||
#
|
||||
# HTTP2 is always streamed for all adapters.
|
||||
defp streaming_compat(conn) do
|
||||
with Phoenix.Endpoint.Cowboy2Adapter <- Pleroma.Web.Endpoint.config(:adapter) do
|
||||
conn
|
||||
else
|
||||
_ -> delete_resp_header(conn, "content-length")
|
||||
end
|
||||
end
|
||||
|
||||
# Only when Tesla adapter is Hackney or Finch does the URL
|
||||
# need encoding before Reverse Proxying as both end up
|
||||
# using the raw Hackney client and cannot leverage our
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@ defmodule Pleroma.Search do
|
|||
alias Pleroma.Workers.SearchIndexingWorker
|
||||
|
||||
@spec add_to_index(Activity.t()) :: {:ok, Oban.Job.t() | :noop} | {:error, Oban.Job.changeset()}
|
||||
def add_to_index(%Activity{id: activity_id, object: %Object{} = object} = activity) do
|
||||
with {_, true} <- {:indexable, indexable?(activity)},
|
||||
{_, "public"} <- {:visibility, Visibility.get_visibility(object)} do
|
||||
def add_to_index(%Activity{id: activity_id, object: %Object{}} = activity) do
|
||||
if indexable?(activity) do
|
||||
SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => activity_id})
|
||||
|> Oban.insert()
|
||||
else
|
||||
_ -> {:ok, :noop}
|
||||
{:ok, :noop}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -41,40 +40,60 @@ defmodule Pleroma.Search do
|
|||
def object_to_search_data(%Object{} = object) do
|
||||
data = object.data
|
||||
|
||||
content_str =
|
||||
case data["content"] do
|
||||
[nil | rest] -> to_string(rest)
|
||||
str -> str
|
||||
end
|
||||
|
||||
content =
|
||||
with {:ok, scrubbed} <-
|
||||
FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing),
|
||||
trimmed <- String.trim(scrubbed) do
|
||||
trimmed
|
||||
end
|
||||
|
||||
# Make sure we have a non-empty string
|
||||
if content != "" do
|
||||
{:ok, published, _} = DateTime.from_iso8601(data["published"])
|
||||
data
|
||||
|> search_texts()
|
||||
|> Enum.map(&sanitize_search_text/1)
|
||||
|> Enum.reject(&(&1 in ["", "."]))
|
||||
|> Enum.join(" ")
|
||||
|
||||
with true <- content != "",
|
||||
published when is_binary(published) <- data["published"],
|
||||
{:ok, published, _} <- DateTime.from_iso8601(published) do
|
||||
%{
|
||||
id: object.id,
|
||||
content: content,
|
||||
ap: data["id"],
|
||||
published: published |> DateTime.to_unix()
|
||||
}
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp indexable?(%Activity{
|
||||
data: %{"type" => "Create"},
|
||||
object: %Object{
|
||||
data: %{"content" => content, "published" => published, "type" => "Note"}
|
||||
}
|
||||
})
|
||||
when not is_nil(content) and content not in ["", "."] and not is_nil(published),
|
||||
do: true
|
||||
def indexable?(%Activity{
|
||||
data: %{"type" => "Create"},
|
||||
object: %Object{data: %{"published" => published, "type" => "Note"}} = object
|
||||
})
|
||||
when not is_nil(published) do
|
||||
Visibility.get_visibility(object) in ["public", "unlisted"] and
|
||||
not is_nil(object_to_search_data(object))
|
||||
end
|
||||
|
||||
defp indexable?(_), do: false
|
||||
def indexable?(_), do: false
|
||||
|
||||
defp search_texts(data) do
|
||||
[data["content"], data["summary"] | attachment_names(data["attachment"])]
|
||||
end
|
||||
|
||||
defp attachment_names(attachments) when is_list(attachments) do
|
||||
Enum.map(attachments, fn
|
||||
%{"summary" => summary} when is_binary(summary) -> summary
|
||||
%{"name" => name} -> name
|
||||
_ -> nil
|
||||
end)
|
||||
end
|
||||
|
||||
defp attachment_names(_), do: []
|
||||
|
||||
defp sanitize_search_text([nil | rest]), do: sanitize_search_text(to_string(rest))
|
||||
|
||||
defp sanitize_search_text(text) when is_binary(text) do
|
||||
case FastSanitize.Sanitizer.scrub(text, Pleroma.HTML.Scrubber.SearchIndexing) do
|
||||
{:ok, scrubbed} -> String.trim(scrubbed)
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp sanitize_search_text(_), do: ""
|
||||
end
|
||||
|
|
|
|||
137
lib/pleroma/search/backfill.ex
Normal file
137
lib/pleroma/search/backfill.ex
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Search.Backfill do
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Workers.SearchIndexingWorker
|
||||
|
||||
@default_chunk_size 100
|
||||
@default_step_size 1_000
|
||||
@default_limit 100_000
|
||||
|
||||
@doc """
|
||||
Enqueues existing Create activities for search indexing.
|
||||
|
||||
The returned `next_cursor` can be passed as `before` to resume a bounded run.
|
||||
Pass `limit: :infinity` to continue until the oldest activity is reached.
|
||||
"""
|
||||
def run(options \\ []) do
|
||||
options =
|
||||
Keyword.validate!(options,
|
||||
chunk: @default_chunk_size,
|
||||
step: @default_step_size,
|
||||
limit: @default_limit,
|
||||
before: nil,
|
||||
on_page: nil
|
||||
)
|
||||
|
||||
chunk_size = positive_option!(options, :chunk, @default_chunk_size)
|
||||
step_size = positive_option!(options, :step, @default_step_size)
|
||||
limit = limit_option!(options)
|
||||
before = cursor_option!(options[:before])
|
||||
on_page = callback_option!(options[:on_page])
|
||||
|
||||
do_run(before, limit, step_size, chunk_size, on_page, 0)
|
||||
end
|
||||
|
||||
defp do_run(before, remaining, step_size, chunk_size, on_page, enqueued) do
|
||||
page_size = page_size(remaining, step_size)
|
||||
ids = fetch_ids(before, page_size)
|
||||
|
||||
case ids do
|
||||
[] ->
|
||||
result(enqueued, before, true)
|
||||
|
||||
ids ->
|
||||
enqueue(ids, chunk_size)
|
||||
|
||||
page_count = length(ids)
|
||||
next_cursor = List.last(ids)
|
||||
total = enqueued + page_count
|
||||
remaining = decrement(remaining, page_count)
|
||||
|
||||
on_page.(%{count: page_count, enqueued: total, next_cursor: next_cursor})
|
||||
|
||||
cond do
|
||||
remaining == 0 -> result(total, next_cursor, false)
|
||||
page_count < page_size -> result(total, next_cursor, true)
|
||||
true -> do_run(next_cursor, remaining, step_size, chunk_size, on_page, total)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_ids(before, limit) do
|
||||
Activity
|
||||
|> where([a], fragment("?->>'type' = 'Create'", a.data))
|
||||
|> maybe_before(before)
|
||||
|> order_by([a], desc: a.id)
|
||||
|> limit(^limit)
|
||||
|> select([a], a.id)
|
||||
|> Pleroma.Repo.all()
|
||||
end
|
||||
|
||||
defp maybe_before(query, nil), do: query
|
||||
defp maybe_before(query, before), do: where(query, [a], a.id < ^before)
|
||||
|
||||
defp enqueue(ids, chunk_size) do
|
||||
ids
|
||||
|> Enum.chunk_every(chunk_size)
|
||||
|> Enum.each(fn chunk ->
|
||||
chunk
|
||||
|> Enum.map(&SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => &1}))
|
||||
|> Oban.insert_all()
|
||||
end)
|
||||
end
|
||||
|
||||
defp page_size(:infinity, step_size), do: step_size
|
||||
defp page_size(remaining, step_size), do: min(remaining, step_size)
|
||||
|
||||
defp decrement(:infinity, _count), do: :infinity
|
||||
defp decrement(remaining, count), do: remaining - count
|
||||
|
||||
defp result(enqueued, next_cursor, exhausted) do
|
||||
%{enqueued: enqueued, next_cursor: next_cursor, exhausted: exhausted}
|
||||
end
|
||||
|
||||
defp positive_option!(options, key, default) do
|
||||
case Keyword.get(options, key, default) do
|
||||
value when is_integer(value) and value > 0 -> value
|
||||
value -> raise ArgumentError, "#{key} must be a positive integer, got: #{inspect(value)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp limit_option!(options) do
|
||||
case Keyword.fetch!(options, :limit) do
|
||||
:infinity -> :infinity
|
||||
value when is_integer(value) and value > 0 -> value
|
||||
value -> raise ArgumentError, "limit must be a positive integer, got: #{inspect(value)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp cursor_option!(nil), do: nil
|
||||
|
||||
defp cursor_option!(cursor) do
|
||||
valid? =
|
||||
try do
|
||||
match?({:ok, <<_::binary-size(16)>>}, FlakeId.Ecto.CompatType.dump(cursor))
|
||||
rescue
|
||||
_ -> false
|
||||
end
|
||||
|
||||
if valid? do
|
||||
cursor
|
||||
else
|
||||
raise ArgumentError, "before must be a valid activity id, got: #{inspect(cursor)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp callback_option!(nil), do: fn _ -> :ok end
|
||||
defp callback_option!(callback) when is_function(callback, 1), do: callback
|
||||
|
||||
defp callback_option!(callback) do
|
||||
raise ArgumentError, "on_page must be a function of arity 1, got: #{inspect(callback)}"
|
||||
end
|
||||
end
|
||||
287
lib/pleroma/search/parade_db.ex
Normal file
287
lib/pleroma/search/parade_db.ex
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Search.ParadeDB do
|
||||
@behaviour Pleroma.Search.SearchBackend
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
import Pleroma.Search.DatabaseSearch,
|
||||
only: [
|
||||
maybe_fetch: 3,
|
||||
maybe_restrict_author: 2,
|
||||
maybe_restrict_blocked: 2,
|
||||
maybe_restrict_local: 2
|
||||
]
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config.Getting, as: Config
|
||||
alias Pleroma.Search
|
||||
|
||||
@max_limit 40
|
||||
@default_table "pleroma_search_documents"
|
||||
|
||||
defp client_impl do
|
||||
Config.get([Pleroma.Search.ParadeDB, :client_impl]) ||
|
||||
Pleroma.Search.ParadeDB.Client.Postgres
|
||||
end
|
||||
|
||||
defp table do
|
||||
Config.get([Pleroma.Search.ParadeDB, :table], @default_table)
|
||||
end
|
||||
|
||||
defp fuzzy_distance do
|
||||
case Config.get([Pleroma.Search.ParadeDB, :fuzzy_distance], 0) do
|
||||
value when is_integer(value) and value in 0..2 ->
|
||||
value
|
||||
|
||||
value when is_binary(value) ->
|
||||
case Integer.parse(value) do
|
||||
{parsed, ""} when parsed in 0..2 -> parsed
|
||||
_ -> 0
|
||||
end
|
||||
|
||||
_ ->
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def create_index do
|
||||
with {:ok, _} <- client_impl().query("CREATE EXTENSION IF NOT EXISTS pg_search", []),
|
||||
{:ok, _} <- client_impl().query(create_table_sql(table()), []),
|
||||
{:ok, _} <- client_impl().query(create_object_id_index_sql(table()), []),
|
||||
{:ok, _} <- client_impl().query(create_bm25_index_sql(table()), []) do
|
||||
:ok
|
||||
else
|
||||
{:error, error} ->
|
||||
Logger.error("ParadeDB create_index failed: #{inspect(error)}")
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def drop_index do
|
||||
case client_impl().query("DROP TABLE IF EXISTS #{table()}", []) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("ParadeDB drop_index failed: #{inspect(error)}")
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def healthcheck_endpoints, do: nil
|
||||
|
||||
@impl true
|
||||
def add_to_index(activity) do
|
||||
with true <- Search.indexable?(activity),
|
||||
maybe_search_data when not is_nil(maybe_search_data) <-
|
||||
Search.object_to_search_data(activity.object) do
|
||||
dumped_activity_id = dump_activity_id(activity.id)
|
||||
actor_ap_id = activity.data["actor"]
|
||||
published_at = DateTime.from_unix!(maybe_search_data.published)
|
||||
|
||||
if is_nil(dumped_activity_id) do
|
||||
Logger.error("ParadeDB add_to_index failed: invalid activity id #{inspect(activity.id)}")
|
||||
:ok
|
||||
else
|
||||
sql =
|
||||
"""
|
||||
INSERT INTO #{table()} (id, object_id, object_ap_id, actor_ap_id, content, published_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
object_id = EXCLUDED.object_id,
|
||||
object_ap_id = EXCLUDED.object_ap_id,
|
||||
actor_ap_id = EXCLUDED.actor_ap_id,
|
||||
content = EXCLUDED.content,
|
||||
published_at = EXCLUDED.published_at
|
||||
"""
|
||||
|
||||
params = [
|
||||
dumped_activity_id,
|
||||
maybe_search_data.id,
|
||||
maybe_search_data.ap,
|
||||
actor_ap_id,
|
||||
maybe_search_data.content,
|
||||
published_at
|
||||
]
|
||||
|
||||
case client_impl().query(sql, params) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("ParadeDB add_to_index failed: #{inspect(error)}")
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
else
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def remove_from_index(object) do
|
||||
sql = "DELETE FROM #{table()} WHERE object_id = $1"
|
||||
|
||||
case client_impl().query(sql, [object.id]) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("ParadeDB remove_from_index failed: #{inspect(error)}")
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def search(user, query, options \\ []) do
|
||||
limit = options |> Keyword.get(:limit, @max_limit) |> min(@max_limit)
|
||||
offset = Keyword.get(options, :offset, 0)
|
||||
author = Keyword.get(options, :author)
|
||||
|
||||
case search_ids(query, limit, offset, author) do
|
||||
{:ok, []} ->
|
||||
maybe_fetch([], user, query)
|
||||
|
||||
{:ok, ids} ->
|
||||
ids
|
||||
|> fetch_activities(user, query, author)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("ParadeDB search failed: #{inspect(error)}")
|
||||
maybe_fetch([], user, query)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_activities(ids, user, query, author) do
|
||||
dumped_ids =
|
||||
ids
|
||||
|> Enum.map(&dump_activity_id/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
from(a in Activity, where: a.id in ^dumped_ids)
|
||||
|> Activity.with_preloaded_object()
|
||||
|> Activity.restrict_deactivated_users()
|
||||
|> maybe_restrict_public(user)
|
||||
|> maybe_restrict_local(user)
|
||||
|> maybe_restrict_author(author)
|
||||
|> maybe_restrict_blocked(user)
|
||||
|> order_by(
|
||||
[a],
|
||||
fragment(
|
||||
"array_position(?, ?)",
|
||||
^dumped_ids,
|
||||
a.id
|
||||
)
|
||||
)
|
||||
|> Pleroma.Repo.all()
|
||||
|> maybe_fetch(user, query)
|
||||
end
|
||||
|
||||
defp maybe_restrict_public(query, %Pleroma.User{}) do
|
||||
intended_recipients = [
|
||||
Pleroma.Constants.as_public(),
|
||||
Pleroma.Web.ActivityPub.Utils.as_local_public()
|
||||
]
|
||||
|
||||
from(a in query,
|
||||
where: fragment("?->>'type' = 'Create'", a.data),
|
||||
where: fragment("? && ?", ^intended_recipients, a.recipients)
|
||||
)
|
||||
end
|
||||
|
||||
defp maybe_restrict_public(query, _user) do
|
||||
from(a in query,
|
||||
where: fragment("?->>'type' = 'Create'", a.data),
|
||||
where: ^Pleroma.Constants.as_public() in a.recipients
|
||||
)
|
||||
end
|
||||
|
||||
defp search_ids(query, limit, offset, author) do
|
||||
base_sql =
|
||||
"SELECT id FROM #{table()} WHERE content &&& #{search_query_param_sql(fuzzy_distance())}"
|
||||
|
||||
params = [query]
|
||||
|
||||
{base_sql, params} =
|
||||
case author do
|
||||
%Pleroma.User{ap_id: ap_id} when is_binary(ap_id) ->
|
||||
{base_sql <> " AND actor_ap_id = $2", params ++ [ap_id]}
|
||||
|
||||
_ ->
|
||||
{base_sql, params}
|
||||
end
|
||||
|
||||
limit_arg = length(params) + 1
|
||||
offset_arg = length(params) + 2
|
||||
|
||||
sql =
|
||||
base_sql <>
|
||||
" ORDER BY published_at DESC, id DESC LIMIT $#{limit_arg} OFFSET $#{offset_arg}"
|
||||
|
||||
params = params ++ [limit, offset]
|
||||
|
||||
with {:ok, %{rows: rows}} <- client_impl().query(sql, params) do
|
||||
{:ok, Enum.map(rows, fn [id] -> id end)}
|
||||
end
|
||||
end
|
||||
|
||||
defp search_query_param_sql(0), do: "$1"
|
||||
defp search_query_param_sql(distance), do: "($1::text)::pdb.fuzzy(#{distance})"
|
||||
|
||||
defp create_table_sql(table) do
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS #{table} (
|
||||
id uuid PRIMARY KEY,
|
||||
object_id bigint NOT NULL,
|
||||
object_ap_id text NOT NULL,
|
||||
actor_ap_id text,
|
||||
content text NOT NULL,
|
||||
published_at timestamptz
|
||||
)
|
||||
"""
|
||||
end
|
||||
|
||||
defp create_object_id_index_sql(table) do
|
||||
"CREATE INDEX IF NOT EXISTS #{table}_object_id_idx ON #{table} (object_id)"
|
||||
end
|
||||
|
||||
defp create_bm25_index_sql(table) do
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS #{table}_bm25_idx ON #{table}
|
||||
USING bm25 (id, content, (actor_ap_id::pdb.literal), published_at)
|
||||
WITH (key_field = 'id')
|
||||
"""
|
||||
end
|
||||
|
||||
defp dump_activity_id(<<_::binary-size(16)>> = uuid), do: uuid
|
||||
|
||||
defp dump_activity_id(id) when is_binary(id) do
|
||||
case Ecto.UUID.dump(id) do
|
||||
{:ok, <<_::binary-size(16)>> = uuid} ->
|
||||
uuid
|
||||
|
||||
:error ->
|
||||
dump_flake_activity_id(id)
|
||||
end
|
||||
end
|
||||
|
||||
defp dump_activity_id(_), do: nil
|
||||
|
||||
defp dump_flake_activity_id(id) do
|
||||
case FlakeId.Ecto.CompatType.dump(id) do
|
||||
{:ok, <<_::binary-size(16)>> = uuid} -> uuid
|
||||
_ -> nil
|
||||
end
|
||||
rescue
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
24
lib/pleroma/search/parade_db/client.ex
Normal file
24
lib/pleroma/search/parade_db/client.ex
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Search.ParadeDB.Client do
|
||||
@moduledoc false
|
||||
|
||||
@type sql :: String.t()
|
||||
@type params :: list()
|
||||
@type result :: %{optional(:rows) => list(list())}
|
||||
|
||||
@callback query(sql(), params()) :: {:ok, result()} | {:error, any()}
|
||||
end
|
||||
|
||||
defmodule Pleroma.Search.ParadeDB.Client.Postgres do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour Pleroma.Search.ParadeDB.Client
|
||||
|
||||
@impl true
|
||||
def query(sql, params \\ []) do
|
||||
Ecto.Adapters.SQL.query(Pleroma.Search.ParadeDB.Repo, sql, params)
|
||||
end
|
||||
end
|
||||
24
lib/pleroma/search/parade_db/repo.ex
Normal file
24
lib/pleroma/search/parade_db/repo.ex
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Search.ParadeDB.Repo do
|
||||
use Ecto.Repo,
|
||||
otp_app: :pleroma,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
migration_timestamps: [type: :naive_datetime_usec]
|
||||
|
||||
@doc """
|
||||
Loads the ParadeDB database URL from `PARADEDB_DATABASE_URL`.
|
||||
|
||||
If the env var is not set, falls back to `config :pleroma, Pleroma.Search.ParadeDB, url: ...`.
|
||||
"""
|
||||
def init(_, opts) do
|
||||
config = Application.get_env(:pleroma, Pleroma.Search.ParadeDB, [])
|
||||
|
||||
url = System.get_env("PARADEDB_DATABASE_URL") || config[:url]
|
||||
url = if is_nil(url), do: nil, else: to_string(url)
|
||||
|
||||
{:ok, Keyword.put(opts, :url, url)}
|
||||
end
|
||||
end
|
||||
|
|
@ -39,6 +39,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.OAuth
|
||||
alias Pleroma.Web.RelMe
|
||||
alias Pleroma.Webhook.Notify
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
alias Pleroma.Workers.DeleteWorker
|
||||
alias Pleroma.Workers.UserRefreshWorker
|
||||
|
|
@ -326,7 +327,7 @@ defmodule Pleroma.User do
|
|||
@spec visible_for(User.t(), User.t() | nil) ::
|
||||
:visible
|
||||
| :invisible
|
||||
| :restricted_unauthenticated
|
||||
| :restrict_unauthenticated
|
||||
| :deactivated
|
||||
| :confirmation_pending
|
||||
def visible_for(user, for_user \\ nil)
|
||||
|
|
@ -987,6 +988,7 @@ defmodule Pleroma.User do
|
|||
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
|
||||
def register(%Ecto.Changeset{} = changeset) do
|
||||
with {:ok, user} <- Repo.insert(changeset) do
|
||||
Notify.trigger_webhooks(user, :"account.created")
|
||||
post_register_action(user)
|
||||
end
|
||||
end
|
||||
|
|
@ -1970,6 +1972,12 @@ defmodule Pleroma.User do
|
|||
|
||||
def approve(%User{} = user), do: {:ok, user}
|
||||
|
||||
def reject(%User{is_approved: false} = user) do
|
||||
delete(user)
|
||||
end
|
||||
|
||||
def reject(%User{} = _user), do: {:error, "User is approved"}
|
||||
|
||||
def confirm(users) when is_list(users) do
|
||||
Repo.transaction(fn ->
|
||||
Enum.map(users, fn user ->
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ defmodule Pleroma.User.Query do
|
|||
limit: pos_integer(),
|
||||
actor_types: [String.t()],
|
||||
birthday_day: pos_integer(),
|
||||
birthday_month: pos_integer()
|
||||
birthday_month: pos_integer(),
|
||||
staff: boolean()
|
||||
}
|
||||
| map()
|
||||
|
||||
|
|
@ -179,6 +180,10 @@ defmodule Pleroma.User.Query do
|
|||
|
||||
defp compose_query({:external, _}, query), do: location_query(query, false)
|
||||
|
||||
defp compose_query({:active, false}, query) do
|
||||
where(query, [u], u.is_active == false or u.is_approved == false or u.is_confirmed == false)
|
||||
end
|
||||
|
||||
defp compose_query({:active, _}, query) do
|
||||
where(query, [u], u.is_active == true)
|
||||
|> where([u], u.is_approved == true)
|
||||
|
|
@ -202,6 +207,10 @@ defmodule Pleroma.User.Query do
|
|||
where(query, [u], u.is_confirmed != ^bool)
|
||||
end
|
||||
|
||||
defp compose_query({:need_approval, false}, query) do
|
||||
where(query, [u], u.is_approved == true)
|
||||
end
|
||||
|
||||
defp compose_query({:need_approval, _}, query) do
|
||||
where(query, [u], u.is_approved == false)
|
||||
end
|
||||
|
|
@ -285,6 +294,19 @@ defmodule Pleroma.User.Query do
|
|||
|> where([u], fragment("date_part('month', ?)", u.birthday) == ^month)
|
||||
end
|
||||
|
||||
defp compose_query({:staff, false}, query) do
|
||||
where(query, [u], u.is_admin == false and u.is_moderator == false)
|
||||
end
|
||||
|
||||
defp compose_query({:staff, _}, query) do
|
||||
where(query, [u], u.is_admin == true or u.is_moderator == true)
|
||||
end
|
||||
|
||||
defp compose_query({:domain, domain}, query) do
|
||||
query
|
||||
|> where([u], like(u.nickname, ^"%@#{domain}"))
|
||||
end
|
||||
|
||||
defp compose_query(_unsupported_param, query), do: query
|
||||
|
||||
defp location_query(query, local) do
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
import Ecto.Query
|
||||
import Pleroma.Web.ActivityPub.Utils
|
||||
import Pleroma.Web.ActivityPub.Visibility
|
||||
import Pleroma.Webhook.Notify, only: [trigger_webhooks: 2]
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
|
@ -415,7 +416,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
with flag_data <- make_flag_data(params, additional),
|
||||
{:ok, activity} <- insert(flag_data, local),
|
||||
_ <- notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
_ <- trigger_webhooks(activity, :"report.created"),
|
||||
:ok <-
|
||||
maybe_federate(activity) do
|
||||
User.all_users_with_privilege(:reports_manage_reports)
|
||||
|> Enum.filter(fn user -> user.ap_id != actor end)
|
||||
|> Enum.filter(fn user -> not is_nil(user.email) end)
|
||||
|
|
@ -710,6 +713,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> Map.put(:type, ["Create", "Announce"])
|
||||
|> Map.put(:user, reading_user)
|
||||
|> Map.put(:actor_id, user.ap_id)
|
||||
|> Map.put(:actor_local, user.local)
|
||||
|> Map.put(:pinned_object_ids, Map.keys(user.pinned_objects))
|
||||
|
||||
params =
|
||||
|
|
@ -1016,6 +1020,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_state(query, _), do: query
|
||||
|
||||
defp restrict_report_target(query, %{report_target_id: actor_id}) do
|
||||
from(activity in query,
|
||||
where: fragment("?->'object'->>0 = ?", activity.data, ^actor_id)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_report_target(query, _), do: query
|
||||
|
||||
defp restrict_assigned_account(query, %{assigned_account: assigned_account}) do
|
||||
from(activity in query,
|
||||
where: fragment("?->>'assigned_account' = ?", activity.data, ^assigned_account)
|
||||
|
|
@ -1349,6 +1361,50 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_unauthenticated(query, _), do: query
|
||||
|
||||
defp restrict_activity_and_object_origin(query, local?) do
|
||||
local_object_prefix = Pleroma.Web.Endpoint.url() <> "/"
|
||||
|
||||
# Keeping the correlated origin checks inside CASE prevents PostgreSQL from choosing a
|
||||
# global bitmap-and-sort plan before applying the account statuses limit.
|
||||
from([activity, object: object] in query,
|
||||
where:
|
||||
fragment(
|
||||
"CASE WHEN ? = ? AND starts_with((?->>'id'), ?) = ? THEN true ELSE false END",
|
||||
activity.local,
|
||||
^local?,
|
||||
object.data,
|
||||
^local_object_prefix,
|
||||
^local?
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp maybe_restrict_unauthenticated(
|
||||
query,
|
||||
%{restrict_unauthenticated: true, user: nil, actor_local: actor_local}
|
||||
)
|
||||
when is_boolean(actor_local) do
|
||||
local_restricted = Config.restrict_unauthenticated_access?(:activities, :local)
|
||||
remote_restricted = Config.restrict_unauthenticated_access?(:activities, :remote)
|
||||
actor_restricted = if actor_local, do: local_restricted, else: remote_restricted
|
||||
|
||||
cond do
|
||||
actor_restricted ->
|
||||
from(activity in query, where: false)
|
||||
|
||||
local_restricted ->
|
||||
restrict_activity_and_object_origin(query, false)
|
||||
|
||||
remote_restricted ->
|
||||
restrict_activity_and_object_origin(query, true)
|
||||
|
||||
true ->
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_restrict_unauthenticated(query, _), do: query
|
||||
|
||||
defp restrict_quote_url(query, %{quote_url: quote_url}) do
|
||||
from([_activity, object] in query,
|
||||
where: fragment("(?)->'quoteUrl' = ?", object.data, ^quote_url)
|
||||
|
|
@ -1506,6 +1562,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> maybe_preload_report_notes(opts)
|
||||
|> maybe_set_thread_muted_field(opts)
|
||||
|> maybe_order(opts)
|
||||
|> maybe_restrict_unauthenticated(opts)
|
||||
|> restrict_recipients_or_hashtags(recipients, opts[:user], opts[:followed_hashtags])
|
||||
|> restrict_replies(opts)
|
||||
|> restrict_since(opts)
|
||||
|
|
@ -1514,6 +1571,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> restrict_actor(opts)
|
||||
|> restrict_type(opts)
|
||||
|> restrict_state(opts)
|
||||
|> restrict_report_target(opts)
|
||||
|> restrict_assigned_account(opts)
|
||||
|> restrict_favorited_by(opts)
|
||||
|> restrict_blocked(restrict_blocked_opts)
|
||||
|
|
|
|||
|
|
@ -309,7 +309,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
} = conn,
|
||||
%{"nickname" => nickname} = params
|
||||
) do
|
||||
with {:recipient_exists, %User{} = recipient} <-
|
||||
with false <- unsupported_http_message_signature?(conn),
|
||||
{:recipient_exists, %User{} = recipient} <-
|
||||
{:recipient_exists, User.get_cached_by_nickname(nickname)},
|
||||
{:sender_exists, {:ok, %User{} = actor}} <-
|
||||
{:sender_exists, User.get_or_fetch_by_ap_id(params["actor"])},
|
||||
|
|
@ -320,6 +321,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
Federator.incoming_ap_doc(params)
|
||||
json(conn, "ok")
|
||||
else
|
||||
true ->
|
||||
reject_unsupported_http_message_signature(conn)
|
||||
|
||||
{:recipient_exists, _} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|
|
@ -348,20 +352,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, params) do
|
||||
Federator.incoming_ap_doc(params)
|
||||
json(conn, "ok")
|
||||
if unsupported_http_message_signature?(conn) do
|
||||
reject_unsupported_http_message_signature(conn)
|
||||
else
|
||||
Federator.incoming_ap_doc(params)
|
||||
json(conn, "ok")
|
||||
end
|
||||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
|
||||
Federator.incoming_failed_signature_ap_doc(%{
|
||||
method: conn.method,
|
||||
req_headers: conn.req_headers,
|
||||
request_path: conn.request_path,
|
||||
params: params,
|
||||
query_string: conn.query_string
|
||||
})
|
||||
if unsupported_http_message_signature?(conn) do
|
||||
reject_unsupported_http_message_signature(conn)
|
||||
else
|
||||
Federator.incoming_failed_signature_ap_doc(%{
|
||||
method: conn.method,
|
||||
req_headers: conn.req_headers,
|
||||
request_path: conn.request_path,
|
||||
params: params,
|
||||
query_string: conn.query_string
|
||||
})
|
||||
|
||||
json(conn, "ok")
|
||||
json(conn, "ok")
|
||||
end
|
||||
end
|
||||
|
||||
# POST /relay/inbox -or- POST /internal/fetch/inbox
|
||||
|
|
@ -381,6 +393,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|> json("error, missing HTTP Signature")
|
||||
end
|
||||
|
||||
defp reject_unsupported_http_message_signature(conn) do
|
||||
conn
|
||||
|> put_status(:unauthorized)
|
||||
|> json("error, unsupported HTTP Message Signature")
|
||||
end
|
||||
|
||||
defp unsupported_http_message_signature?(conn) do
|
||||
signature_headers = Plug.Conn.get_req_header(conn, "signature")
|
||||
|
||||
multiple_signature_headers?(signature_headers) or
|
||||
(Plug.Conn.get_req_header(conn, "signature-input") != [] and
|
||||
not cavage_signature?(signature_headers))
|
||||
end
|
||||
|
||||
defp multiple_signature_headers?([_, _ | _]), do: true
|
||||
defp multiple_signature_headers?(_), do: false
|
||||
|
||||
defp cavage_signature?(signature_headers) do
|
||||
Enum.any?(signature_headers, &Regex.match?(~r/(^|,)\s*keyId\s*=/, &1))
|
||||
end
|
||||
|
||||
defp post_inbox_relayed_create(conn, params) do
|
||||
Logger.debug(
|
||||
"Signature missing or not from author, relayed Create message, fetching object from source"
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
when objtype in ~w[Question Answer Audio Video Image Event Article Note Page] do
|
||||
with {:ok, object_data} <-
|
||||
object
|
||||
|> CommonFixes.maybe_set_attributed_to_from_activity(create_activity)
|
||||
|> CommonFixes.maybe_add_language_from_activity(create_activity)
|
||||
|> cast_and_apply_and_stringify_with_history(),
|
||||
meta = Keyword.put(meta, :object_data, object_data),
|
||||
|
|
@ -172,6 +173,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
{_, {:ok, object_data, _}} <-
|
||||
{:object_validation,
|
||||
object
|
||||
|> CommonFixes.maybe_set_attributed_to_from_activity(update_activity)
|
||||
|> CommonFixes.maybe_add_language_from_activity(update_activity)
|
||||
|> validate(meta)},
|
||||
meta = Keyword.put(meta, :object_data, object_data),
|
||||
|
|
|
|||
|
|
@ -50,7 +50,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
end
|
||||
|
||||
defp fix_url(%{"url" => url} = data) when is_bitstring(url), do: data
|
||||
defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"])
|
||||
|
||||
defp fix_url(%{"url" => url} = data) when is_map(url) do
|
||||
if is_binary(url["href"]) do
|
||||
Map.put(data, "url", url["href"])
|
||||
else
|
||||
Map.delete(data, "url")
|
||||
end
|
||||
end
|
||||
|
||||
defp fix_url(%{"url" => url} = data) when is_list(url) do
|
||||
first_element = Enum.at(url, 0)
|
||||
|
||||
url_string =
|
||||
cond do
|
||||
is_bitstring(first_element) -> first_element
|
||||
is_map(first_element) -> first_element["href"]
|
||||
true -> nil
|
||||
end
|
||||
|
||||
if is_binary(url_string) do
|
||||
Map.put(data, "url", url_string)
|
||||
else
|
||||
Map.delete(data, "url")
|
||||
end
|
||||
end
|
||||
|
||||
defp fix_url(data), do: data
|
||||
|
||||
defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do
|
||||
|
|
@ -79,10 +104,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
|
||||
defp fix_replies(data), do: Map.delete(data, "replies")
|
||||
|
||||
def fix_attachments(%{"attachment" => attachment} = data) when is_map(attachment),
|
||||
do: Map.put(data, "attachment", [attachment])
|
||||
|
||||
def fix_attachments(data), do: data
|
||||
def fix_attachments(data), do: Transmogrifier.fix_attachments(data)
|
||||
|
||||
defp remote_mention_resolver(
|
||||
%{"id" => ap_id, "tag" => tags},
|
||||
|
|
|
|||
|
|
@ -41,22 +41,34 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
def fix_object_defaults(data) do
|
||||
data = Maps.filter_empty_values(data)
|
||||
|
||||
in_reply_to_id = in_reply_to_id(data["inReplyTo"])
|
||||
|
||||
context =
|
||||
Utils.maybe_create_context(
|
||||
data["context"] || data["conversation"] || data["inReplyTo"] || data["id"]
|
||||
data["context"] || data["conversation"] || in_reply_to_id || data["id"]
|
||||
)
|
||||
|
||||
%User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"])
|
||||
data = Map.put(data, "context", context)
|
||||
|
||||
data
|
||||
|> Map.put("context", context)
|
||||
|> cast_and_filter_recipients("to", follower_collection)
|
||||
|> cast_and_filter_recipients("cc", follower_collection)
|
||||
|> cast_and_filter_recipients("bto", follower_collection)
|
||||
|> cast_and_filter_recipients("bcc", follower_collection)
|
||||
|> Transmogrifier.fix_implicit_addressing(follower_collection)
|
||||
with attributed_to when is_binary(attributed_to) <- data["attributedTo"],
|
||||
%User{follower_address: follower_collection} <- User.get_cached_by_ap_id(attributed_to) do
|
||||
data
|
||||
|> cast_and_filter_recipients("to", follower_collection)
|
||||
|> cast_and_filter_recipients("cc", follower_collection)
|
||||
|> cast_and_filter_recipients("bto", follower_collection)
|
||||
|> cast_and_filter_recipients("bcc", follower_collection)
|
||||
|> Transmogrifier.fix_implicit_addressing(follower_collection)
|
||||
else
|
||||
_ -> data
|
||||
end
|
||||
end
|
||||
|
||||
defp in_reply_to_id(in_reply_to) when is_binary(in_reply_to), do: in_reply_to
|
||||
defp in_reply_to_id(%{"id" => in_reply_to}) when is_binary(in_reply_to), do: in_reply_to
|
||||
defp in_reply_to_id(%{"href" => in_reply_to}) when is_binary(in_reply_to), do: in_reply_to
|
||||
defp in_reply_to_id([in_reply_to | _]), do: in_reply_to_id(in_reply_to)
|
||||
defp in_reply_to_id(_), do: nil
|
||||
|
||||
def fix_activity_addressing(activity) do
|
||||
%User{follower_address: follower_collection} = User.get_cached_by_ap_id(activity["actor"])
|
||||
|
||||
|
|
@ -79,6 +91,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
|> Map.put("attributedTo", actor)
|
||||
end
|
||||
|
||||
def maybe_set_attributed_to_from_activity(object, activity) when is_map(object) do
|
||||
if is_nil(object["attributedTo"]) and is_nil(object["actor"]) do
|
||||
case Containment.get_actor(activity) do
|
||||
actor when is_binary(actor) -> Map.put(object, "attributedTo", actor)
|
||||
_ -> object
|
||||
end
|
||||
else
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
def fix_activity_context(data, %Object{data: %{"context" => object_context}}) do
|
||||
data
|
||||
|> Map.put("context", object_context)
|
||||
|
|
@ -96,23 +119,37 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
Map.put(data, "to", to)
|
||||
end
|
||||
|
||||
def fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data
|
||||
def fix_quote_url(%{"quoteUrl" => quote_url} = data) do
|
||||
case normalize_object_id(quote_url) do
|
||||
quote_url when is_binary(quote_url) -> Map.put(data, "quoteUrl", quote_url)
|
||||
_ -> Map.delete(data, "quoteUrl")
|
||||
end
|
||||
end
|
||||
|
||||
# Fedibird
|
||||
# https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
|
||||
def fix_quote_url(%{"quoteUri" => quote_url} = data) do
|
||||
Map.put(data, "quoteUrl", quote_url)
|
||||
case normalize_object_id(quote_url) do
|
||||
quote_url when is_binary(quote_url) -> Map.put(data, "quoteUrl", quote_url)
|
||||
_ -> data
|
||||
end
|
||||
end
|
||||
|
||||
# Old Fedibird (bug)
|
||||
# https://github.com/fedibird/mastodon/issues/9
|
||||
def fix_quote_url(%{"quoteURL" => quote_url} = data) do
|
||||
Map.put(data, "quoteUrl", quote_url)
|
||||
case normalize_object_id(quote_url) do
|
||||
quote_url when is_binary(quote_url) -> Map.put(data, "quoteUrl", quote_url)
|
||||
_ -> data
|
||||
end
|
||||
end
|
||||
|
||||
# Misskey fallback
|
||||
def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do
|
||||
Map.put(data, "quoteUrl", quote_url)
|
||||
case normalize_object_id(quote_url) do
|
||||
quote_url when is_binary(quote_url) -> Map.put(data, "quoteUrl", quote_url)
|
||||
_ -> data
|
||||
end
|
||||
end
|
||||
|
||||
def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do
|
||||
|
|
@ -128,6 +165,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
|
||||
def fix_quote_url(data), do: data
|
||||
|
||||
defp normalize_object_id(object_id) when is_binary(object_id), do: object_id
|
||||
defp normalize_object_id(%{"id" => object_id}) when is_binary(object_id), do: object_id
|
||||
defp normalize_object_id([object_id | _]) when is_binary(object_id), do: object_id
|
||||
defp normalize_object_id(_), do: nil
|
||||
|
||||
# On Mastodon, `"likes"` attribute includes an inlined `Collection` with `totalItems`,
|
||||
# not a list of users.
|
||||
# https://github.com/mastodon/mastodon/pull/32007
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
|
|||
require Pleroma.Constants
|
||||
|
||||
@primary_key false
|
||||
@tag_types ~w[Mention Hashtag Emoji Link]
|
||||
|
||||
embedded_schema do
|
||||
# Common
|
||||
field(:type, :string)
|
||||
|
|
@ -49,7 +51,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
|
|||
|> validate_required([:type, :href])
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => "Hashtag", "name" => name} = data) do
|
||||
def changeset(struct, %{"type" => "Hashtag", "name" => name} = data) when is_binary(name) do
|
||||
name = String.downcase(name)
|
||||
data = Map.put(data, "name", name)
|
||||
|
||||
|
|
@ -58,13 +60,42 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
|
|||
|> validate_required([:type, :name])
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => "Emoji"} = data) do
|
||||
data = Map.put(data, "name", String.trim(data["name"], ":"))
|
||||
|
||||
def changeset(struct, %{"type" => "Hashtag"} = data) do
|
||||
struct
|
||||
|> cast(data, [:type, :name, :updated, :id])
|
||||
|> cast_embed(:icon, with: &icon_changeset/2)
|
||||
|> validate_required([:type, :name, :icon])
|
||||
|> cast(data, [])
|
||||
|> Map.put(:action, :ignore)
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => "Emoji", "name" => name} = data) when is_binary(name) do
|
||||
data =
|
||||
data
|
||||
|> Map.put("name", String.trim(data["name"], ":"))
|
||||
|> normalize_emoji_icon()
|
||||
|
||||
case data["icon"] do
|
||||
%{"url" => url} when is_binary(url) ->
|
||||
if valid_http_url?(url) do
|
||||
struct
|
||||
|> cast(data, [:type, :name, :updated, :id])
|
||||
|> cast_embed(:icon, with: &icon_changeset/2)
|
||||
|> validate_required([:type, :name, :icon])
|
||||
else
|
||||
struct
|
||||
|> cast(data, [])
|
||||
|> Map.put(:action, :ignore)
|
||||
end
|
||||
|
||||
_ ->
|
||||
struct
|
||||
|> cast(data, [])
|
||||
|> Map.put(:action, :ignore)
|
||||
end
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => "Emoji"} = data) do
|
||||
struct
|
||||
|> cast(data, [])
|
||||
|> Map.put(:action, :ignore)
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => "Link"} = data) do
|
||||
|
|
@ -74,12 +105,80 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
|
|||
|> validate_required([:type, :href, :mediaType])
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => types} = data) when is_list(types) do
|
||||
changeset(struct, infer_type(data))
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => _} = data) do
|
||||
struct
|
||||
|> cast(data, [])
|
||||
|> Map.put(:action, :ignore)
|
||||
end
|
||||
|
||||
def changeset(struct, data) when is_map(data) do
|
||||
data = normalize(data)
|
||||
|
||||
if Map.has_key?(data, "type") do
|
||||
changeset(struct, data)
|
||||
else
|
||||
struct
|
||||
|> cast(data, [])
|
||||
|> Map.put(:action, :ignore)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(data) when is_map(data), do: infer_type(data)
|
||||
|
||||
defp infer_type(%{"type" => types} = data) when is_list(types) do
|
||||
type = Enum.find(types, &(&1 in @tag_types)) || Enum.find(types, &is_binary/1)
|
||||
|
||||
if type, do: Map.put(data, "type", type), else: Map.delete(data, "type")
|
||||
end
|
||||
|
||||
defp infer_type(%{"type" => _} = data), do: data
|
||||
defp infer_type(%{"name" => "#" <> _} = data), do: Map.put(data, "type", "Hashtag")
|
||||
defp infer_type(%{"name" => "@" <> _} = data), do: Map.put(data, "type", "Mention")
|
||||
|
||||
defp infer_type(%{"mediaType" => _media_type, "href" => _href} = data),
|
||||
do: Map.put(data, "type", "Link")
|
||||
|
||||
defp infer_type(data), do: data
|
||||
|
||||
defp normalize_emoji_icon(%{"icon" => icon} = data) when is_binary(icon) do
|
||||
Map.put(data, "icon", %{"type" => "Image", "url" => icon})
|
||||
end
|
||||
|
||||
defp normalize_emoji_icon(%{"icon" => icon} = data) when is_map(icon) do
|
||||
case extract_icon_url(icon) do
|
||||
url when is_binary(url) -> Map.put(data, "icon", %{"type" => "Image", "url" => url})
|
||||
_ -> Map.delete(data, "icon")
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_emoji_icon(data), do: data
|
||||
|
||||
defp extract_icon_url(%{"url" => url}) when is_binary(url), do: url
|
||||
|
||||
defp extract_icon_url(%{"url" => %{"href" => href}}) when is_binary(href), do: href
|
||||
|
||||
defp extract_icon_url(%{"url" => [first | _]}) do
|
||||
cond do
|
||||
is_binary(first) -> first
|
||||
is_map(first) -> first["href"]
|
||||
true -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_icon_url(%{"href" => href}) when is_binary(href), do: href
|
||||
|
||||
defp extract_icon_url(_), do: nil
|
||||
|
||||
defp valid_http_url?(url) when is_binary(url) do
|
||||
match?({:ok, _}, ObjectValidators.Uri.cast(url))
|
||||
end
|
||||
|
||||
defp valid_http_url?(_), do: false
|
||||
|
||||
def icon_changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, [:type, :url])
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
|
@ -28,6 +29,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
require Pleroma.Constants
|
||||
|
||||
@supported_types Pleroma.Constants.activity_types() ++
|
||||
Pleroma.Constants.object_types() ++ Pleroma.Constants.actor_types()
|
||||
|
||||
@doc """
|
||||
Modifies an incoming AP object (mastodon format) to our internal format.
|
||||
"""
|
||||
|
|
@ -201,9 +205,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
|
||||
when not is_nil(in_reply_to) do
|
||||
in_reply_to_id = prepare_in_reply_to(in_reply_to)
|
||||
|
||||
object =
|
||||
cond do
|
||||
is_list(in_reply_to) ->
|
||||
if not_empty_string(in_reply_to_id) do
|
||||
Map.put(object, "inReplyTo", in_reply_to_id)
|
||||
else
|
||||
Map.delete(object, "inReplyTo")
|
||||
end
|
||||
|
||||
is_map(in_reply_to) and is_nil(in_reply_to["id"]) and is_binary(in_reply_to["href"]) ->
|
||||
Map.put(object, "inReplyTo", in_reply_to["href"])
|
||||
|
||||
is_map(in_reply_to) and is_nil(in_reply_to["id"]) and is_nil(in_reply_to["href"]) ->
|
||||
Map.delete(object, "inReplyTo")
|
||||
|
||||
true ->
|
||||
object
|
||||
end
|
||||
|
||||
depth = (options[:depth] || 0) + 1
|
||||
|
||||
if Federator.allowed_thread_distance?(depth) do
|
||||
if not_empty_string(in_reply_to_id) and Federator.allowed_thread_distance?(depth) do
|
||||
with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
|
||||
%Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
|
||||
object
|
||||
|
|
@ -249,9 +273,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
|
||||
in_reply_to["id"]
|
||||
|
||||
is_map(in_reply_to) && is_bitstring(in_reply_to["href"]) ->
|
||||
in_reply_to["href"]
|
||||
|
||||
is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
|
||||
Enum.at(in_reply_to, 0)
|
||||
|
||||
is_list(in_reply_to) && is_map(Enum.at(in_reply_to, 0)) ->
|
||||
in_reply_to
|
||||
|> Enum.at(0)
|
||||
|> prepare_in_reply_to()
|
||||
|
||||
true ->
|
||||
""
|
||||
end
|
||||
|
|
@ -265,59 +297,74 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.drop(["conversation"])
|
||||
end
|
||||
|
||||
defp valid_http_url?(href) when is_binary(href) do
|
||||
match?({:ok, _}, ObjectValidators.Uri.cast(href))
|
||||
end
|
||||
|
||||
defp valid_http_url?(_), do: false
|
||||
|
||||
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
|
||||
attachments =
|
||||
Enum.map(attachment, fn data ->
|
||||
url =
|
||||
cond do
|
||||
is_list(data["url"]) -> List.first(data["url"])
|
||||
is_map(data["url"]) -> data["url"]
|
||||
true -> nil
|
||||
end
|
||||
Enum.map(attachment, fn
|
||||
data when is_map(data) ->
|
||||
url =
|
||||
cond do
|
||||
is_list(data["url"]) -> List.first(data["url"])
|
||||
is_map(data["url"]) -> data["url"]
|
||||
true -> nil
|
||||
end
|
||||
|
||||
media_type =
|
||||
cond do
|
||||
is_map(url) && url =~ Pleroma.Constants.mime_regex() ->
|
||||
url["mediaType"]
|
||||
url_data = if is_map(url), do: url, else: %{}
|
||||
|
||||
is_bitstring(data["mediaType"]) && data["mediaType"] =~ Pleroma.Constants.mime_regex() ->
|
||||
data["mediaType"]
|
||||
media_type =
|
||||
cond do
|
||||
is_bitstring(url_data["mediaType"]) &&
|
||||
url_data["mediaType"] =~ Pleroma.Constants.mime_regex() ->
|
||||
url_data["mediaType"]
|
||||
|
||||
is_bitstring(data["mimeType"]) && data["mimeType"] =~ Pleroma.Constants.mime_regex() ->
|
||||
data["mimeType"]
|
||||
is_bitstring(data["mediaType"]) &&
|
||||
data["mediaType"] =~ Pleroma.Constants.mime_regex() ->
|
||||
data["mediaType"]
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
is_bitstring(data["mimeType"]) && data["mimeType"] =~ Pleroma.Constants.mime_regex() ->
|
||||
data["mimeType"]
|
||||
|
||||
href =
|
||||
cond do
|
||||
is_map(url) && is_binary(url["href"]) -> url["href"]
|
||||
is_binary(data["url"]) -> data["url"]
|
||||
is_binary(data["href"]) -> data["href"]
|
||||
true -> nil
|
||||
end
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
|
||||
href =
|
||||
cond do
|
||||
is_binary(url) -> url
|
||||
is_binary(url_data["href"]) -> url_data["href"]
|
||||
is_binary(data["url"]) -> data["url"]
|
||||
is_binary(data["href"]) -> data["href"]
|
||||
true -> nil
|
||||
end
|
||||
|
||||
if href && valid_http_url?(href) do
|
||||
attachment_url =
|
||||
%{
|
||||
"href" => href,
|
||||
"type" => Map.get(url_data, "type", "Link")
|
||||
}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("width", url_data["width"] || data["width"])
|
||||
|> Maps.put_if_present("height", url_data["height"] || data["height"])
|
||||
|
||||
if href do
|
||||
attachment_url =
|
||||
%{
|
||||
"href" => href,
|
||||
"type" => Map.get(url || %{}, "type", "Link")
|
||||
"url" => [attachment_url],
|
||||
"type" => data["type"] || "Document"
|
||||
}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("width", (url || %{})["width"] || data["width"])
|
||||
|> Maps.put_if_present("height", (url || %{})["height"] || data["height"])
|
||||
|> Maps.put_if_present("name", data["name"])
|
||||
|> Maps.put_if_present("blurhash", data["blurhash"])
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
%{
|
||||
"url" => [attachment_url],
|
||||
"type" => data["type"] || "Document"
|
||||
}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("name", data["name"])
|
||||
|> Maps.put_if_present("blurhash", data["blurhash"])
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
|
|
@ -351,24 +398,58 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def fix_url(object), do: object
|
||||
|
||||
defp emoji_icon_url(icon) when is_binary(icon), do: icon
|
||||
|
||||
defp emoji_icon_url(%{"url" => url}) when is_binary(url), do: url
|
||||
|
||||
defp emoji_icon_url(%{"url" => %{"href" => href}}) when is_binary(href), do: href
|
||||
|
||||
defp emoji_icon_url(%{"url" => [first | _]}) do
|
||||
cond do
|
||||
is_binary(first) -> first
|
||||
is_map(first) -> first["href"] || first["url"] || first["id"]
|
||||
true -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp emoji_icon_url(%{"href" => href}) when is_binary(href), do: href
|
||||
|
||||
defp emoji_icon_url(_), do: nil
|
||||
|
||||
defp valid_emoji_icon_url(icon) do
|
||||
case emoji_icon_url(icon) do
|
||||
url when is_binary(url) -> if valid_http_url?(url), do: url
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
|
||||
emoji =
|
||||
tags
|
||||
|> Enum.filter(fn data -> is_map(data) and data["type"] == "Emoji" and data["icon"] end)
|
||||
|> Enum.filter(fn data ->
|
||||
is_map(data) and data["type"] == "Emoji" and is_binary(data["name"]) and data["icon"]
|
||||
end)
|
||||
|> Enum.reduce(%{}, fn data, mapping ->
|
||||
name = String.trim(data["name"], ":")
|
||||
|
||||
Map.put(mapping, name, data["icon"]["url"])
|
||||
case valid_emoji_icon_url(data["icon"]) do
|
||||
url when is_binary(url) -> Map.put(mapping, name, url)
|
||||
_ -> mapping
|
||||
end
|
||||
end)
|
||||
|
||||
Map.put(object, "emoji", emoji)
|
||||
end
|
||||
|
||||
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
|
||||
name = String.trim(tag["name"], ":")
|
||||
emoji = %{name => tag["icon"]["url"]}
|
||||
def fix_emoji(%{"tag" => %{"type" => "Emoji", "name" => name} = tag} = object)
|
||||
when is_binary(name) do
|
||||
name = String.trim(name, ":")
|
||||
|
||||
Map.put(object, "emoji", emoji)
|
||||
with url when is_binary(url) <- valid_emoji_icon_url(tag["icon"]) do
|
||||
Map.put(object, "emoji", %{name => url})
|
||||
else
|
||||
_ -> object
|
||||
end
|
||||
end
|
||||
|
||||
def fix_emoji(object), do: object
|
||||
|
|
@ -376,7 +457,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
|
||||
tags =
|
||||
tag
|
||||
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|
||||
|> Enum.filter(fn data ->
|
||||
is_map(data) and data["type"] == "Hashtag" and is_binary(data["name"])
|
||||
end)
|
||||
|> Enum.map(fn
|
||||
%{"name" => "#" <> hashtag} -> String.downcase(hashtag)
|
||||
%{"name" => hashtag} -> String.downcase(hashtag)
|
||||
|
|
@ -439,9 +522,37 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def handle_incoming(data, options \\ []) do
|
||||
data
|
||||
|> fix_recursive(&strip_internal_fields/1)
|
||||
|> fix_recursive(&fix_type_as_string/1)
|
||||
|> handle_incoming_normalized(options)
|
||||
end
|
||||
|
||||
defp fix_type_as_string(data) do
|
||||
data
|
||||
|> normalize_type()
|
||||
|> normalize_tag_types()
|
||||
end
|
||||
|
||||
defp normalize_type(%{"type" => types} = data) when is_list(types) do
|
||||
type = Enum.find(types, &(&1 in @supported_types)) || Enum.find(types, &is_binary/1)
|
||||
|
||||
if type, do: Map.put(data, "type", type), else: data
|
||||
end
|
||||
|
||||
defp normalize_type(data), do: data
|
||||
|
||||
defp normalize_tag_types(%{"tag" => tags} = data) when is_list(tags) do
|
||||
Map.put(data, "tag", Enum.map(tags, &normalize_tag_type/1))
|
||||
end
|
||||
|
||||
defp normalize_tag_types(%{"tag" => tag} = data) when is_map(tag) do
|
||||
Map.put(data, "tag", normalize_tag_type(tag))
|
||||
end
|
||||
|
||||
defp normalize_tag_types(data), do: data
|
||||
|
||||
defp normalize_tag_type(tag) when is_map(tag), do: TagValidator.normalize(tag)
|
||||
defp normalize_tag_type(tag), do: tag
|
||||
|
||||
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
|
||||
# with nil ID.
|
||||
defp handle_incoming_normalized(
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
Pleroma.Docs.JSON.compiled_descriptions()
|
||||
|> Enum.filter(&allowed_config?(&1, :whitelist))
|
||||
|> Enum.reject(&allowed_config?(&1, :blacklist))
|
||||
|> Enum.reject(&ConfigDB.static?/1)
|
||||
|
||||
json(conn, translate_descriptions(descriptions))
|
||||
end
|
||||
|
|
@ -92,7 +93,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
|
||||
def show(conn, _params) do
|
||||
with :ok <- configurable_from_database() do
|
||||
configs = ConfigDB.get_all_as_keyword()
|
||||
configs = ConfigDB.get_all_as_keyword(exclude_static: true)
|
||||
|
||||
merged =
|
||||
Config.Holder.default_config()
|
||||
|
|
@ -123,6 +124,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
end)
|
||||
end)
|
||||
|> List.flatten()
|
||||
|> Enum.reject(&ConfigDB.static?/1)
|
||||
|
||||
render(conn, "index.json", %{
|
||||
configs: merged,
|
||||
|
|
@ -137,6 +139,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
configs
|
||||
|> Enum.filter(&allowed_config?(&1, :whitelist))
|
||||
|> Enum.reject(&allowed_config?(&1, :blacklist))
|
||||
|> Enum.reject(&(ConfigDB.static?(&1) and not &1[:delete]))
|
||||
|> Enum.map(fn
|
||||
%{group: group, key: key, delete: true} = params ->
|
||||
ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]})
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ defmodule Pleroma.Web.AdminAPI.FallbackController do
|
|||
|> json(%{error: dgettext("errors", "Not found")})
|
||||
end
|
||||
|
||||
def call(conn, {:error, :forbidden}) do
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{error: dgettext("errors", "Forbidden")})
|
||||
end
|
||||
|
||||
def call(conn, {:error, reason}) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|
|
|
|||
|
|
@ -8,13 +8,17 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
import Pleroma.Web.ControllerHelper,
|
||||
only: [fetch_integer_param: 3]
|
||||
|
||||
alias Ecto.Multi
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.PasswordResetToken
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.AdminAPI
|
||||
alias Pleroma.Web.AdminAPI.Search
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
@users_page_size 50
|
||||
|
||||
|
|
@ -148,9 +152,25 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
} = conn,
|
||||
_
|
||||
) do
|
||||
changesets =
|
||||
conn =
|
||||
if Enum.any?(users, fn user -> not Map.has_key?(user, :password) end) do
|
||||
put_private(conn, :skip_idempotency_cache, true)
|
||||
else
|
||||
conn
|
||||
end
|
||||
|
||||
multi =
|
||||
users
|
||||
|> Enum.map(fn %{nickname: nickname, email: email, password: password} ->
|
||||
|> Enum.map(fn %{nickname: nickname, email: email} = attrs ->
|
||||
{password, passwordless?} =
|
||||
case Map.fetch(attrs, :password) do
|
||||
{:ok, password} ->
|
||||
{password, false}
|
||||
|
||||
:error ->
|
||||
{:crypto.strong_rand_bytes(16) |> Base.encode64(), true}
|
||||
end
|
||||
|
||||
user_data = %{
|
||||
nickname: nickname,
|
||||
name: nickname,
|
||||
|
|
@ -160,44 +180,80 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
bio: "."
|
||||
}
|
||||
|
||||
User.register_changeset(%User{}, user_data, need_confirmation: false)
|
||||
{User.register_changeset(%User{}, user_data, confirmed: true), passwordless?}
|
||||
end)
|
||||
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
|
||||
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
|
||||
|> Enum.reduce(Multi.new(), fn {changeset, passwordless?}, multi ->
|
||||
user_operation = {:user, Ecto.UUID.generate()}
|
||||
multi = Multi.insert(multi, user_operation, changeset)
|
||||
|
||||
if passwordless? do
|
||||
Multi.run(
|
||||
multi,
|
||||
{:password_reset_token, Ecto.UUID.generate()},
|
||||
fn _repo, changes ->
|
||||
changes
|
||||
|> Map.fetch!(user_operation)
|
||||
|> PasswordResetToken.create_token()
|
||||
end
|
||||
)
|
||||
else
|
||||
multi
|
||||
end
|
||||
end)
|
||||
|
||||
case Pleroma.Repo.transaction(changesets) do
|
||||
{:ok, users_map} ->
|
||||
users =
|
||||
users_map
|
||||
case Pleroma.Repo.transaction(multi) do
|
||||
{:ok, results} ->
|
||||
{users, password_reset_tokens} =
|
||||
results
|
||||
|> Map.values()
|
||||
|> Enum.split_with(&match?(%User{}, &1))
|
||||
|
||||
users =
|
||||
users
|
||||
|> Enum.map(fn user ->
|
||||
{:ok, user} = User.post_register_action(user)
|
||||
|
||||
user
|
||||
end)
|
||||
|
||||
password_reset_links =
|
||||
Map.new(password_reset_tokens, fn token ->
|
||||
{token.user_id, Router.Helpers.reset_password_url(Endpoint, :reset, token.token)}
|
||||
end)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subjects: users,
|
||||
action: "create"
|
||||
})
|
||||
|
||||
render(conn, "created_many.json", users: users)
|
||||
render(conn, "created_many.json",
|
||||
users: users,
|
||||
password_reset_links: password_reset_links
|
||||
)
|
||||
|
||||
{:error, id, changeset, _} ->
|
||||
{:error, {:user, _} = id, %Ecto.Changeset{} = changeset, _} ->
|
||||
changesets =
|
||||
Enum.map(changesets.operations, fn
|
||||
Enum.flat_map(multi.operations, fn
|
||||
{^id, {:changeset, _current_changeset, _}} ->
|
||||
changeset
|
||||
[changeset]
|
||||
|
||||
{_, {:changeset, current_changeset, _}} ->
|
||||
current_changeset
|
||||
{{:user, _}, {:changeset, current_changeset, _}} ->
|
||||
[current_changeset]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end)
|
||||
|
||||
conn
|
||||
|> put_status(:conflict)
|
||||
|> render("create_errors.json", changesets: changesets)
|
||||
|
||||
{:error, {:password_reset_token, _}, _reason, _} ->
|
||||
Pleroma.Web.AdminAPI.FallbackController.call(
|
||||
conn,
|
||||
{:password_reset_token_error, :creation_failed}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
101
lib/pleroma/web/admin_api/controllers/webhook_controller.ex
Normal file
101
lib/pleroma/web/admin_api/controllers/webhook_controller.ex
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.WebhookController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Webhook
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["admin:write"]}
|
||||
when action in [:update, :create, :delete, :enable, :disable, :rotate_secret]
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action in [:index, :show])
|
||||
|
||||
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.WebhookOperation
|
||||
|
||||
def index(conn, _) do
|
||||
webhooks =
|
||||
Webhook
|
||||
|> Repo.all()
|
||||
|
||||
render(conn, "index.json", webhooks: webhooks)
|
||||
end
|
||||
|
||||
def show(conn, %{id: id}) do
|
||||
with %Webhook{} = webhook <- Webhook.get(id) do
|
||||
render(conn, "show.json", webhook: webhook)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def create(%{body_params: params} = conn, _) do
|
||||
with {:ok, webhook} <- Webhook.create(webhook_params(params)) do
|
||||
render(conn, "show.json", webhook: webhook)
|
||||
end
|
||||
end
|
||||
|
||||
def update(%{body_params: params} = conn, %{id: id}) do
|
||||
with %Webhook{internal: false} = webhook <- Webhook.get(id),
|
||||
{:ok, webhook} <- Webhook.update(webhook, webhook_params(params)) do
|
||||
render(conn, "show.json", webhook: webhook)
|
||||
else
|
||||
%Webhook{internal: true} -> {:error, :forbidden}
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{id: id}) do
|
||||
with %Webhook{internal: false} = webhook <- Webhook.get(id),
|
||||
{:ok, webhook} <- Webhook.delete(webhook) do
|
||||
render(conn, "show.json", webhook: webhook)
|
||||
else
|
||||
%Webhook{internal: true} -> {:error, :forbidden}
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def enable(conn, %{id: id}) do
|
||||
with %Webhook{internal: false} = webhook <- Webhook.get(id),
|
||||
{:ok, webhook} <- Webhook.set_enabled(webhook, true) do
|
||||
render(conn, "show.json", webhook: webhook)
|
||||
else
|
||||
%Webhook{internal: true} -> {:error, :forbidden}
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def disable(conn, %{id: id}) do
|
||||
with %Webhook{internal: false} = webhook <- Webhook.get(id),
|
||||
{:ok, webhook} <- Webhook.set_enabled(webhook, false) do
|
||||
render(conn, "show.json", webhook: webhook)
|
||||
else
|
||||
%Webhook{internal: true} -> {:error, :forbidden}
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def rotate_secret(conn, %{id: id}) do
|
||||
with %Webhook{internal: false} = webhook <- Webhook.get(id),
|
||||
{:ok, webhook} <- Webhook.rotate_secret(webhook) do
|
||||
render(conn, "show.json", webhook: webhook)
|
||||
else
|
||||
%Webhook{internal: true} -> {:error, :forbidden}
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp webhook_params(params) do
|
||||
Map.drop(params, [:internal, "internal"])
|
||||
end
|
||||
end
|
||||
|
|
@ -88,18 +88,29 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
|
|||
}
|
||||
end
|
||||
|
||||
def render("created_many.json", %{users: users}) do
|
||||
render_many(users, AccountView, "created.json", as: :user)
|
||||
def render("created_many.json", %{users: users} = assigns) do
|
||||
password_reset_links = Map.get(assigns, :password_reset_links, %{})
|
||||
|
||||
render_many(users, AccountView, "created.json",
|
||||
as: :user,
|
||||
password_reset_links: password_reset_links
|
||||
)
|
||||
end
|
||||
|
||||
def render("created.json", %{user: user}) do
|
||||
%{
|
||||
type: "success",
|
||||
code: 200,
|
||||
data: %{
|
||||
def render("created.json", %{user: user} = assigns) do
|
||||
password_reset_links = Map.get(assigns, :password_reset_links, %{})
|
||||
|
||||
data =
|
||||
%{
|
||||
nickname: user.nickname,
|
||||
email: user.email
|
||||
}
|
||||
|> maybe_put_password_reset_link(Map.get(password_reset_links, user.id))
|
||||
|
||||
%{
|
||||
type: "success",
|
||||
code: 200,
|
||||
data: data
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -119,6 +130,11 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
|
|||
}
|
||||
end
|
||||
|
||||
defp maybe_put_password_reset_link(data, nil), do: data
|
||||
|
||||
defp maybe_put_password_reset_link(data, link),
|
||||
do: Map.put(data, :password_reset_link, link)
|
||||
|
||||
def merge_account_views(%User{} = user) do
|
||||
MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|
||||
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
|
||||
|
|
|
|||
34
lib/pleroma/web/admin_api/views/webhook_view.ex
Normal file
34
lib/pleroma/web/admin_api/views/webhook_view.ex
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.WebhookView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
def render("index.json", %{webhooks: webhooks}) do
|
||||
render_many(webhooks, __MODULE__, "show.json")
|
||||
end
|
||||
|
||||
def render("show.json", %{webhook: webhook}) do
|
||||
%{
|
||||
id: webhook.id |> to_string(),
|
||||
url: webhook.url,
|
||||
events: webhook.events,
|
||||
secret: webhook.secret,
|
||||
enabled: webhook.enabled,
|
||||
internal: webhook.internal,
|
||||
created_at: Utils.to_masto_date(webhook.inserted_at),
|
||||
updated_at: Utils.to_masto_date(webhook.updated_at)
|
||||
}
|
||||
end
|
||||
|
||||
def render("event.json", %{type: type, object: object}) do
|
||||
%{
|
||||
event: type,
|
||||
created_at: Utils.to_masto_date(NaiveDateTime.utc_now()),
|
||||
object: object
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -105,7 +105,15 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"Report management",
|
||||
"Status administration",
|
||||
"User administration",
|
||||
"Announcement management"
|
||||
"Announcement management",
|
||||
"Webhooks"
|
||||
]
|
||||
},
|
||||
%{
|
||||
"name" => "Administration (Mastodon API)",
|
||||
"tags" => [
|
||||
"User administration (Mastodon API)",
|
||||
"Report management (Mastodon API)"
|
||||
]
|
||||
},
|
||||
%{"name" => "Applications", "tags" => ["Applications", "Push subscriptions"]},
|
||||
|
|
|
|||
|
|
@ -82,8 +82,15 @@ defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do
|
|||
properties: %{
|
||||
nickname: %Schema{type: :string},
|
||||
email: %Schema{type: :string},
|
||||
password: %Schema{type: :string}
|
||||
}
|
||||
password: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Optional. When omitted, a `password_reset_link` is returned, " <>
|
||||
"allowing the user to set their own password. When provided, " <>
|
||||
"it must not be empty."
|
||||
}
|
||||
},
|
||||
required: [:nickname, :email]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -102,7 +109,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do
|
|||
type: :object,
|
||||
properties: %{
|
||||
email: %Schema{type: :string, format: :email},
|
||||
nickname: %Schema{type: :string}
|
||||
nickname: %Schema{type: :string},
|
||||
password_reset_link: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
208
lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex
Normal file
208
lib/pleroma/web/api_spec/operations/admin/webhook_operation.ex
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Admin.WebhookOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Webhooks"],
|
||||
summary: "Retrieve a list of webhooks",
|
||||
operationId: "AdminAPI.WebhookController.index",
|
||||
security: [%{"oAuth" => ["admin:read"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of webhooks", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: webhook()
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Webhooks"],
|
||||
summary: "Retrieve a webhook",
|
||||
operationId: "AdminAPI.WebhookController.show",
|
||||
security: [%{"oAuth" => ["admin:read"]}],
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Webhook", "application/json", webhook())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Webhooks"],
|
||||
summary: "Create a webhook",
|
||||
operationId: "AdminAPI.WebhookController.create",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
description: "POST body for creating a webhook",
|
||||
type: :object,
|
||||
required: [:url, :events],
|
||||
properties: %{
|
||||
url: %Schema{type: :string, format: :uri, maxLength: 255},
|
||||
events: event_type(min_items: 1),
|
||||
enabled: %Schema{type: :boolean}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
),
|
||||
responses: %{
|
||||
200 => Operation.response("Webhook", "application/json", webhook())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Webhooks"],
|
||||
summary: "Update a webhook",
|
||||
operationId: "AdminAPI.WebhookController.update",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: [id_param()],
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
description: "POST body for updating a webhook",
|
||||
type: :object,
|
||||
properties: %{
|
||||
url: %Schema{type: :string, format: :uri, maxLength: 255},
|
||||
events: event_type(min_items: 1),
|
||||
enabled: %Schema{type: :boolean}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
),
|
||||
responses: %{
|
||||
200 => Operation.response("Webhook", "application/json", webhook()),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Webhooks"],
|
||||
summary: "Delete a webhook",
|
||||
operationId: "AdminAPI.WebhookController.delete",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Webhook", "application/json", webhook()),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def enable_operation do
|
||||
%Operation{
|
||||
tags: ["Webhooks"],
|
||||
summary: "Enable a webhook",
|
||||
operationId: "AdminAPI.WebhookController.enable",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Webhook", "application/json", webhook()),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def disable_operation do
|
||||
%Operation{
|
||||
tags: ["Webhooks"],
|
||||
summary: "Disable a webhook",
|
||||
operationId: "AdminAPI.WebhookController.disable",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Webhook", "application/json", webhook()),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def rotate_secret_operation do
|
||||
%Operation{
|
||||
tags: ["Webhooks"],
|
||||
summary: "Rotate webhook signing secret",
|
||||
operationId: "AdminAPI.WebhookController.rotate_secret",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Webhook", "application/json", webhook()),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp webhook do
|
||||
%Schema{
|
||||
title: "Webhook",
|
||||
description: "Schema for a webhook",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
url: %Schema{type: :string, format: :uri, maxLength: 255},
|
||||
events: event_type(),
|
||||
secret: %Schema{type: :string},
|
||||
enabled: %Schema{type: :boolean},
|
||||
internal: %Schema{type: :boolean},
|
||||
created_at: %Schema{type: :string, format: :"date-time"},
|
||||
updated_at: %Schema{type: :string, format: :"date-time"}
|
||||
},
|
||||
example: %{
|
||||
"id" => "1",
|
||||
"url" => "https://example.com/webhook",
|
||||
"events" => ["report.created"],
|
||||
"secret" => "d3d8cf4bc11fd9c41fd34dcc38d282e451c8bd34",
|
||||
"enabled" => true,
|
||||
"internal" => false,
|
||||
"created_at" => "2022-06-24T16:19:38.523Z",
|
||||
"updated_at" => "2022-06-24T16:19:38.523Z"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp event_type(opts \\ []) do
|
||||
schema = %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
title: "Event",
|
||||
description: "Event type",
|
||||
type: :string,
|
||||
enum: ["account.created", "report.created"]
|
||||
}
|
||||
}
|
||||
|
||||
case Keyword.fetch(opts, :min_items) do
|
||||
{:ok, min_items} -> %{schema | minItems: min_items}
|
||||
:error -> schema
|
||||
end
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, :string, "Webhook ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["User administration (Mastodon API)"],
|
||||
summary: "View accounts by criteria (v1)",
|
||||
operationId: "MastodonAdmin.AccountController.index",
|
||||
description: "View accounts matching certain criteria for filtering, up to 40 at a time.",
|
||||
security: [%{"oAuth" => ["admin:read:accounts"]}],
|
||||
parameters:
|
||||
[
|
||||
Operation.parameter(:local, :query, :boolean, "Filter for local accounts?"),
|
||||
Operation.parameter(:remote, :query, :boolean, "Filter for remote accounts?"),
|
||||
Operation.parameter(
|
||||
:active,
|
||||
:query,
|
||||
:boolean,
|
||||
"Filter for currently active accounts?"
|
||||
),
|
||||
Operation.parameter(
|
||||
:pending,
|
||||
:query,
|
||||
:boolean,
|
||||
"Filter for currently pending accounts?"
|
||||
),
|
||||
Operation.parameter(
|
||||
:disabled,
|
||||
:query,
|
||||
:boolean,
|
||||
"Filter for currently disabled accounts?"
|
||||
),
|
||||
Operation.parameter(
|
||||
:silenced,
|
||||
:query,
|
||||
:boolean,
|
||||
"Filter for currently silenced accounts? (not implemented yet)"
|
||||
),
|
||||
Operation.parameter(
|
||||
:suspended,
|
||||
:query,
|
||||
:boolean,
|
||||
"Filter for currently suspended accounts? (not implemented yet)"
|
||||
),
|
||||
Operation.parameter(
|
||||
:sensitized,
|
||||
:query,
|
||||
:boolean,
|
||||
"Filter for accounts force-marked as sensitive? (not implemented yet)"
|
||||
),
|
||||
Operation.parameter(:username, :query, :string, "Search for the given username"),
|
||||
Operation.parameter(
|
||||
:display_name,
|
||||
:query,
|
||||
:string,
|
||||
"Search for the given display name"
|
||||
),
|
||||
Operation.parameter(
|
||||
:by_domain,
|
||||
:query,
|
||||
:string,
|
||||
"Filter by the given domain"
|
||||
),
|
||||
Operation.parameter(:email, :query, :string, "Lookup a user with this email"),
|
||||
Operation.parameter(
|
||||
:ip,
|
||||
:query,
|
||||
:string,
|
||||
"Lookup users with this IP address (not implemented yet)"
|
||||
),
|
||||
Operation.parameter(:staff, :query, :boolean, "Filter for staff accounts?")
|
||||
] ++
|
||||
pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Account", "application/json", %Schema{
|
||||
title: "ArrayOfAccounts",
|
||||
type: :array,
|
||||
items: account()
|
||||
}),
|
||||
401 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def index2_operation do
|
||||
%Operation{
|
||||
tags: ["User administration (Mastodon API)"],
|
||||
summary: "View accounts by criteria (v2)",
|
||||
operationId: "MastodonAdmin.AccountController.index2",
|
||||
description: "View accounts matching certain criteria for filtering, up to 40 at a time.",
|
||||
security: [%{"oAuth" => ["admin:read:accounts"]}],
|
||||
parameters:
|
||||
[
|
||||
Operation.parameter(
|
||||
:origin,
|
||||
:query,
|
||||
%Schema{type: :string, enum: ["local", "remote"]},
|
||||
"Filter for local or remote accounts"
|
||||
),
|
||||
Operation.parameter(
|
||||
:status,
|
||||
:query,
|
||||
%Schema{
|
||||
type: :string,
|
||||
enum: ["active", "inactive", "pending", "disabled", "silenced", "suspended"]
|
||||
},
|
||||
"Filter for active, pending, disabled, silenced or suspended accounts"
|
||||
),
|
||||
Operation.parameter(
|
||||
:permissions,
|
||||
:query,
|
||||
:string,
|
||||
"Filter for accounts with staff permissions (users that can manage reports)."
|
||||
),
|
||||
Operation.parameter(
|
||||
:role_ids,
|
||||
:query,
|
||||
%Schema{
|
||||
oneOf: [
|
||||
%Schema{type: :array, items: %Schema{type: :string}},
|
||||
%Schema{type: :string}
|
||||
]
|
||||
},
|
||||
"Filter for users with these roles. (not implemented yet)"
|
||||
),
|
||||
Operation.parameter(
|
||||
:invited_by,
|
||||
:query,
|
||||
:string,
|
||||
"Lookup users invited by the account with this ID. (not implemented yet)"
|
||||
),
|
||||
Operation.parameter(:username, :query, :string, "Search for the given username"),
|
||||
Operation.parameter(
|
||||
:display_name,
|
||||
:query,
|
||||
:string,
|
||||
"Search for the given display name"
|
||||
),
|
||||
Operation.parameter(
|
||||
:by_domain,
|
||||
:query,
|
||||
:string,
|
||||
"Filter by the given domain"
|
||||
),
|
||||
Operation.parameter(:email, :query, :string, "Lookup a user with this email"),
|
||||
Operation.parameter(
|
||||
:ip,
|
||||
:query,
|
||||
:string,
|
||||
"Lookup users with this IP address (not implemented yet)"
|
||||
)
|
||||
] ++
|
||||
pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Account", "application/json", %Schema{
|
||||
title: "ArrayOfAccounts",
|
||||
type: :array,
|
||||
items: account()
|
||||
}),
|
||||
401 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["User administration (Mastodon API)"],
|
||||
summary: "View a specific account",
|
||||
operationId: "MastodonAdmin.AccountController.show",
|
||||
description: "View admin-level information about the given account.",
|
||||
security: [%{"oAuth" => ["admin:read:accounts"]}],
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "ID of the account")
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", account()),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def account_action_operation do
|
||||
%Operation{
|
||||
tags: ["User administration (Mastodon API)"],
|
||||
summary: "Perform an action against an account",
|
||||
operationId: "MastodonAdmin.AccountController.account_action",
|
||||
description:
|
||||
"Perform an action against an account and log this action in the moderation history.",
|
||||
security: [%{"oAuth" => ["admin:write:accounts"]}],
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "ID of the account")
|
||||
],
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
type: %Schema{
|
||||
type: :string,
|
||||
enum: ["none", "disable"]
|
||||
},
|
||||
report_id: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "ID of an associated report that caused this action to be taken"
|
||||
}
|
||||
},
|
||||
required: [:type]
|
||||
},
|
||||
required: true
|
||||
),
|
||||
responses: %{
|
||||
204 => no_content_response(),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
403 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["User administration (Mastodon API)"],
|
||||
summary: "Delete a specific account",
|
||||
operationId: "MastodonAdmin.AccountController.delete",
|
||||
description: "Delete the given account.",
|
||||
security: [%{"oAuth" => ["admin:write:accounts"]}],
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "ID of the account")
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", account()),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def enable_operation do
|
||||
%Operation{
|
||||
tags: ["User administration (Mastodon API)"],
|
||||
summary: "Re-enable account",
|
||||
operationId: "MastodonAdmin.AccountController.enable",
|
||||
description: "Re-enable a local account whose login is currently disabled.",
|
||||
security: [%{"oAuth" => ["admin:write:accounts"]}],
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "ID of the account")
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", account()),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def approve_operation do
|
||||
%Operation{
|
||||
tags: ["User administration (Mastodon API)"],
|
||||
summary: "Approve pending account",
|
||||
operationId: "MastodonAdmin.AccountController.approve",
|
||||
description: "Approve the given local account if it is currently pending approval.",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "ID of the account")
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", account()),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def reject_operation do
|
||||
%Operation{
|
||||
tags: ["User administration (Mastodon API)"],
|
||||
summary: "Reject pending account",
|
||||
operationId: "MastodonAdmin.AccountController.reject",
|
||||
description: "Reject the given local account if it is currently pending approval.",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "ID of the account")
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", account()),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def account do
|
||||
%Schema{
|
||||
title: "Admin::Account",
|
||||
description: "Admin-level information about a given account.",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: FlakeID,
|
||||
username: %Schema{type: :string},
|
||||
domain: %Schema{type: :string, nullable: true},
|
||||
created_at: %Schema{type: :string, format: "date-time"},
|
||||
email: %Schema{type: :string, format: "email", nullable: true},
|
||||
ip: %Schema{type: :string, nullable: true},
|
||||
ips: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
ip: %Schema{type: :string},
|
||||
used_at: %Schema{type: :string, format: "date-time"}
|
||||
}
|
||||
}
|
||||
},
|
||||
locale: %Schema{type: :string, nullable: true},
|
||||
invite_request: %Schema{type: :string, nullable: true},
|
||||
role: %Schema{type: :string, nullable: true},
|
||||
confirmed: %Schema{type: :boolean},
|
||||
approved: %Schema{type: :boolean},
|
||||
disabled: %Schema{type: :boolean},
|
||||
silenced: %Schema{type: :boolean, nullable: true},
|
||||
suspended: %Schema{type: :boolean, nullable: true},
|
||||
account: Account,
|
||||
created_by_application_id: %Schema{type: :string, nullable: true},
|
||||
invited_by_account_id: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.MastodonAdmin.ReportOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Report management (Mastodon API)"],
|
||||
summary: "View all reports",
|
||||
operationId: "MastodonAdmin.ReportController.index",
|
||||
description:
|
||||
"View all reports. Pagination may be done with HTTP Link header in the response.",
|
||||
security: [%{"oAuth" => ["admin:read:reports"]}],
|
||||
parameters:
|
||||
[
|
||||
Operation.parameter(:resolved, :query, :boolean, "Filter for resolved reports"),
|
||||
Operation.parameter(:unresolved, :query, :boolean, "Filter for open reports"),
|
||||
Operation.parameter(:account_id, :query, :string, "Filter by author account id"),
|
||||
Operation.parameter(
|
||||
:target_account_id,
|
||||
:query,
|
||||
:string,
|
||||
"Filter by report target account id"
|
||||
)
|
||||
] ++
|
||||
pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Reports", "application/json", %Schema{
|
||||
title: "ArrayOfReports",
|
||||
type: :array,
|
||||
items: report()
|
||||
}),
|
||||
401 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Report management (Mastodon API)"],
|
||||
summary: "View a single report",
|
||||
operationId: "MastodonAdmin.ReportController.show",
|
||||
description: "View information about the report with the given ID.",
|
||||
security: [%{"oAuth" => ["admin:read:reports"]}],
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "ID of the report")
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Report", "application/json", report()),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def resolve_operation do
|
||||
%Operation{
|
||||
tags: ["Report management (Mastodon API)"],
|
||||
summary: "Mark as resolved",
|
||||
operationId: "MastodonAdmin.ReportController.resolve",
|
||||
description: "Mark a report as resolved with no further action taken.",
|
||||
security: [%{"oAuth" => ["admin:write:reports"]}],
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "ID of the report")
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Report", "application/json", report()),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def reopen_operation do
|
||||
%Operation{
|
||||
tags: ["Report management (Mastodon API)"],
|
||||
summary: "Re-open report",
|
||||
operationId: "MastodonAdmin.ReportController.reopen",
|
||||
description: "Reopen a currently closed report.",
|
||||
security: [%{"oAuth" => ["admin:write:reports"]}],
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "ID of the report")
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Report", "application/json", report()),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def assign_to_self_operation do
|
||||
%Operation{
|
||||
tags: ["Report management (Mastodon API)"],
|
||||
summary: "Assign report to self",
|
||||
operationId: "MastodonAdmin.ReportController.assign_to_self",
|
||||
security: [%{"oAuth" => ["admin:write:reports"]}],
|
||||
parameters: [Operation.parameter(:id, :path, :string, "ID of the report")],
|
||||
responses: report_update_responses()
|
||||
}
|
||||
end
|
||||
|
||||
def unassign_operation do
|
||||
%Operation{
|
||||
tags: ["Report management (Mastodon API)"],
|
||||
summary: "Unassign report",
|
||||
operationId: "MastodonAdmin.ReportController.unassign",
|
||||
security: [%{"oAuth" => ["admin:write:reports"]}],
|
||||
parameters: [Operation.parameter(:id, :path, :string, "ID of the report")],
|
||||
responses: report_update_responses()
|
||||
}
|
||||
end
|
||||
|
||||
defp report_update_responses do
|
||||
%{
|
||||
200 => Operation.response("Report", "application/json", report()),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
end
|
||||
|
||||
defp report do
|
||||
%Schema{
|
||||
title: "Admin::Report",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: FlakeID,
|
||||
action_taken: %Schema{type: :boolean},
|
||||
action_taken_at: %Schema{type: :string, format: "date-time", nullable: true},
|
||||
category: %Schema{type: :string, enum: ["spam", "legal", "violation", "other"]},
|
||||
comment: %Schema{type: :string, nullable: true},
|
||||
forwarded: %Schema{type: :boolean, nullable: true},
|
||||
created_at: %Schema{type: :string, format: "date-time"},
|
||||
updated_at: %Schema{type: :string, format: "date-time"},
|
||||
account: AccountOperation.account(),
|
||||
target_account: AccountOperation.account(),
|
||||
assigned_account: %Schema{
|
||||
oneOf: [AccountOperation.account()],
|
||||
nullable: true
|
||||
},
|
||||
action_taken_by_account: %Schema{
|
||||
oneOf: [AccountOperation.account()],
|
||||
nullable: true
|
||||
},
|
||||
statuses: %Schema{
|
||||
type: :array,
|
||||
items: Status
|
||||
},
|
||||
rules: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
text: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
|||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||
|
||||
|
|
@ -304,7 +305,25 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
|||
page_max_id: %Schema{type: :string, nullable: true},
|
||||
latest_page_notification_at: %Schema{type: :string, format: :"date-time", nullable: true},
|
||||
sample_account_ids: %Schema{type: :array, items: %Schema{type: :string}},
|
||||
status_id: %Schema{type: :string, nullable: true}
|
||||
status_id: %Schema{type: :string, nullable: true},
|
||||
target_id: %Schema{
|
||||
type: :string,
|
||||
description: "ID of the target account for a move notification"
|
||||
},
|
||||
emoji: %Schema{
|
||||
type: :string,
|
||||
description: "Emoji used for an emoji reaction notification"
|
||||
},
|
||||
emoji_url: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "URL of the custom emoji used for an emoji reaction notification"
|
||||
},
|
||||
chat_message: ChatMessage,
|
||||
report: %Schema{
|
||||
type: :object,
|
||||
description: "Report embedded in a report notification"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
:group_key,
|
||||
|
|
|
|||
|
|
@ -356,34 +356,6 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def card_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve status information"],
|
||||
deprecated: true,
|
||||
summary: "Preview card",
|
||||
description: "Deprecated in favor of card property inlined on Status entity",
|
||||
operationId: "StatusController.card",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Card", "application/json", %Schema{
|
||||
type: :object,
|
||||
nullable: true,
|
||||
properties: %{
|
||||
type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
|
||||
provider_name: %Schema{type: :string, nullable: true},
|
||||
provider_url: %Schema{type: :string, format: :uri},
|
||||
url: %Schema{type: :string, format: :uri},
|
||||
image: %Schema{type: :string, nullable: true, format: :uri},
|
||||
title: %Schema{type: :string},
|
||||
description: %Schema{type: :string}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def favourited_by_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve status information"],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||
alias Pleroma.Web.ApiSpec.Schemas.PreviewCard
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
|
|
@ -22,36 +23,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
|
|||
emojis: %Schema{type: :array, items: Emoji},
|
||||
attachment: %Schema{type: :object, nullable: true},
|
||||
card: %Schema{
|
||||
type: :object,
|
||||
nullable: true,
|
||||
description: "Preview card for links included within status content",
|
||||
required: [:url, :title, :description, :type],
|
||||
properties: %{
|
||||
type: %Schema{
|
||||
type: :string,
|
||||
enum: ["link", "photo", "video", "rich"],
|
||||
description: "The type of the preview card"
|
||||
},
|
||||
provider_name: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "The provider of the original resource"
|
||||
},
|
||||
provider_url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "A link to the provider of the original resource"
|
||||
},
|
||||
url: %Schema{type: :string, format: :uri, description: "Location of linked resource"},
|
||||
image: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
format: :uri,
|
||||
description: "Preview thumbnail"
|
||||
},
|
||||
title: %Schema{type: :string, description: "Title of linked resource"},
|
||||
description: %Schema{type: :string, description: "Description of preview"}
|
||||
}
|
||||
allOf: [PreviewCard],
|
||||
nullable: true
|
||||
},
|
||||
unread: %Schema{type: :boolean, description: "Whether a message has been marked as read."}
|
||||
},
|
||||
|
|
|
|||
109
lib/pleroma/web/api_spec/schemas/preview_card.ex
Normal file
109
lib/pleroma/web/api_spec/schemas/preview_card.ex
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.PreviewCard do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "PreviewCard",
|
||||
description: "Preview card for a link included within status or chat message content",
|
||||
type: :object,
|
||||
required: [
|
||||
:url,
|
||||
:title,
|
||||
:description,
|
||||
:language,
|
||||
:type,
|
||||
:authors,
|
||||
:author_name,
|
||||
:author_url,
|
||||
:provider_name,
|
||||
:provider_url,
|
||||
:html,
|
||||
:width,
|
||||
:height,
|
||||
:image,
|
||||
:image_description,
|
||||
:embed_url,
|
||||
:blurhash,
|
||||
:published_at
|
||||
],
|
||||
properties: %{
|
||||
url: %Schema{type: :string, format: :uri, description: "Location of linked resource"},
|
||||
title: %Schema{type: :string, description: "Title of linked resource"},
|
||||
description: %Schema{type: :string, description: "Description of preview"},
|
||||
language: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Language of the linked resource"
|
||||
},
|
||||
type: %Schema{
|
||||
type: :string,
|
||||
enum: ["link", "photo", "video", "rich"],
|
||||
description: "The type of the preview card"
|
||||
},
|
||||
authors: %Schema{
|
||||
type: :array,
|
||||
description: "Fediverse accounts of the authors of the original resource",
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
required: [:name, :url, :account],
|
||||
properties: %{
|
||||
name: %Schema{type: :string},
|
||||
url: %Schema{type: :string},
|
||||
account: %Schema{allOf: [Account], nullable: true}
|
||||
}
|
||||
}
|
||||
},
|
||||
author_name: %Schema{type: :string, description: "Author of the original resource"},
|
||||
author_url: %Schema{
|
||||
type: :string,
|
||||
description: "Link to the author of the original resource"
|
||||
},
|
||||
provider_name: %Schema{
|
||||
type: :string,
|
||||
description: "The provider of the original resource"
|
||||
},
|
||||
provider_url: %Schema{
|
||||
type: :string,
|
||||
description: "A link to the provider of the original resource"
|
||||
},
|
||||
html: %Schema{
|
||||
type: :string,
|
||||
format: :html,
|
||||
description: "HTML to be used for generating the preview card"
|
||||
},
|
||||
width: %Schema{type: :integer, description: "Width of preview, in pixels"},
|
||||
height: %Schema{type: :integer, description: "Height of preview, in pixels"},
|
||||
image: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
format: :uri,
|
||||
description: "Preview thumbnail"
|
||||
},
|
||||
image_description: %Schema{
|
||||
type: :string,
|
||||
description: "Alternate text that describes what is in the thumbnail"
|
||||
},
|
||||
embed_url: %Schema{
|
||||
type: :string,
|
||||
description: "Used for photo embeds, instead of custom `html`"
|
||||
},
|
||||
blurhash: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description:
|
||||
"A hash computed by the [BlurHash algorithm](https://github.com/woltapp/blurhash), for generating colorful preview thumbnails when media has not been downloaded yet."
|
||||
},
|
||||
published_at: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Publication date of the linked resource"
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
||||
alias Pleroma.Web.ApiSpec.Schemas.PreviewCard
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Tag
|
||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||
|
||||
|
|
@ -31,40 +32,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
},
|
||||
bookmarked: %Schema{type: :boolean, description: "Have you bookmarked this status?"},
|
||||
card: %Schema{
|
||||
type: :object,
|
||||
nullable: true,
|
||||
description: "Preview card for links included within status content",
|
||||
required: [:url, :title, :description, :type],
|
||||
properties: %{
|
||||
type: %Schema{
|
||||
type: :string,
|
||||
enum: ["link", "photo", "video", "rich"],
|
||||
description: "The type of the preview card"
|
||||
},
|
||||
provider_name: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "The provider of the original resource"
|
||||
},
|
||||
provider_url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "A link to the provider of the original resource"
|
||||
},
|
||||
url: %Schema{type: :string, format: :uri, description: "Location of linked resource"},
|
||||
image: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
format: :uri,
|
||||
description: "Preview thumbnail"
|
||||
},
|
||||
image_description: %Schema{
|
||||
type: :string,
|
||||
description: "Alternate text that describes what is in the thumbnail"
|
||||
},
|
||||
title: %Schema{type: :string, description: "Title of linked resource"},
|
||||
description: %Schema{type: :string, description: "Description of preview"}
|
||||
}
|
||||
allOf: [PreviewCard],
|
||||
nullable: true
|
||||
},
|
||||
content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"},
|
||||
text: %Schema{
|
||||
|
|
|
|||
|
|
@ -700,7 +700,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
|
||||
def update_report_state(activity_id, state) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(activity_id, filter: []) do
|
||||
with %Activity{} = activity <- Activity.get_report(activity_id) do
|
||||
Utils.update_report_state(activity, state)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
|
|
@ -715,7 +715,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
|
||||
def assign_report_to_account(activity_id, user) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||
with %Activity{} = activity <- Activity.get_report(activity_id) do
|
||||
Utils.assign_report_to_account(activity, user)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,305 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.Admin.AccountController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
import Pleroma.Web.ControllerHelper,
|
||||
only: [
|
||||
add_link_headers: 2,
|
||||
assign_account_by_id: 2,
|
||||
json_response: 3
|
||||
]
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
@filter_params ~W(
|
||||
local external active needing_approval deactivated nickname name domain email staff origin status
|
||||
)
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["admin:read:accounts"]}
|
||||
when action in [:index, :index2, :show]
|
||||
)
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["admin:write:accounts"]}
|
||||
when action in [
|
||||
:delete,
|
||||
:enable,
|
||||
:account_action,
|
||||
:approve,
|
||||
:reject
|
||||
]
|
||||
)
|
||||
|
||||
plug(
|
||||
:assign_account_by_id
|
||||
when action in [
|
||||
:show,
|
||||
:delete,
|
||||
:enable,
|
||||
:account_action,
|
||||
:approve,
|
||||
:reject
|
||||
]
|
||||
)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MastodonAdmin.AccountOperation
|
||||
|
||||
def index(conn, params) do
|
||||
users =
|
||||
params
|
||||
|> build_criteria()
|
||||
|> User.Query.build()
|
||||
|> Pagination.fetch_paginated(params)
|
||||
|
||||
conn
|
||||
|> add_link_headers(users)
|
||||
|> render("index.json", users: users)
|
||||
end
|
||||
|
||||
def index2(conn, params) do
|
||||
users =
|
||||
params
|
||||
|> build_criteria_v2()
|
||||
|> User.Query.build()
|
||||
|> Pagination.fetch_paginated(params)
|
||||
|
||||
conn
|
||||
|> add_link_headers(users)
|
||||
|> render("index.json", users: users)
|
||||
end
|
||||
|
||||
def show(%{assigns: %{user: _admin, account: user}} = conn, _params) do
|
||||
render(conn, "show.json", user: user)
|
||||
end
|
||||
|
||||
def account_action(
|
||||
%{assigns: %{user: admin, account: user}, body_params: %{type: type} = body_params} =
|
||||
conn,
|
||||
_params
|
||||
) do
|
||||
with :ok <- validate_account_action(user, type),
|
||||
{:ok, report} <- associated_report(user, body_params),
|
||||
:ok <- resolve_report(admin, report),
|
||||
{:ok, _user} <- handle_account_action(user, admin, type) do
|
||||
json_response(conn, :no_content, "")
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, error} ->
|
||||
json_response(conn, :bad_request, %{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
def delete(%{assigns: %{user: admin, account: user}} = conn, _params) do
|
||||
{:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
|
||||
Pipeline.common_pipeline(delete_data, local: true)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: [user],
|
||||
action: "delete"
|
||||
})
|
||||
|
||||
render(conn, "show.json", user: user)
|
||||
end
|
||||
|
||||
def enable(%{assigns: %{user: admin, account: user}} = conn, _params) do
|
||||
{:ok, user} = User.set_activation(user, true)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: [user],
|
||||
action: "activate"
|
||||
})
|
||||
|
||||
render(conn, "show.json", user: user)
|
||||
end
|
||||
|
||||
def approve(%{assigns: %{user: admin, account: user}} = conn, _params) do
|
||||
{:ok, user} = User.approve(user)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: [user],
|
||||
action: "approve"
|
||||
})
|
||||
|
||||
render(conn, "show.json", user: user)
|
||||
end
|
||||
|
||||
def reject(%{assigns: %{user: admin, account: user}} = conn, _params) do
|
||||
with {:ok, _} <- User.reject(user) do
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: [user],
|
||||
action: "reject"
|
||||
})
|
||||
|
||||
render(conn, "show.json", user: user)
|
||||
else
|
||||
{:error, error} ->
|
||||
json_response(conn, :bad_request, %{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_account_action(%User{local: true} = user, admin, "disable") do
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: [user],
|
||||
action: "deactivate"
|
||||
})
|
||||
|
||||
User.set_activation(user, false)
|
||||
end
|
||||
|
||||
defp handle_account_action(%User{}, _admin, "disable") do
|
||||
{:error, "Only local accounts can be disabled"}
|
||||
end
|
||||
|
||||
defp handle_account_action(user, _admin, "none") do
|
||||
{:ok, user}
|
||||
end
|
||||
|
||||
defp validate_account_action(%User{local: true}, "disable"), do: :ok
|
||||
|
||||
defp validate_account_action(%User{}, "disable"),
|
||||
do: {:error, "Only local accounts can be disabled"}
|
||||
|
||||
defp validate_account_action(%User{}, "none"), do: :ok
|
||||
|
||||
defp build_criteria(params) do
|
||||
%{}
|
||||
|> maybe_filter_local(params)
|
||||
|> maybe_filter_external(params)
|
||||
|> maybe_filter_active(params)
|
||||
|> maybe_filter_needing_approval(params)
|
||||
|> maybe_filter_deactivated(params)
|
||||
|> maybe_filter_nickname(params)
|
||||
|> maybe_filter_name(params)
|
||||
|> maybe_filter_domain(params)
|
||||
|> maybe_filter_email(params)
|
||||
|> maybe_filter_staff(params)
|
||||
end
|
||||
|
||||
defp build_criteria_v2(params) do
|
||||
%{}
|
||||
|> maybe_filter_origin(params)
|
||||
|> maybe_filter_status(params)
|
||||
|> maybe_filter_staff(params)
|
||||
|> maybe_filter_nickname(params)
|
||||
|> maybe_filter_name(params)
|
||||
|> maybe_filter_domain(params)
|
||||
|> maybe_filter_email(params)
|
||||
end
|
||||
|
||||
defp maybe_filter_local(criteria, %{local: true} = _params),
|
||||
do: Map.put(criteria, :local, true)
|
||||
|
||||
defp maybe_filter_local(criteria, %{local: false} = _params),
|
||||
do: Map.put(criteria, :external, true)
|
||||
|
||||
defp maybe_filter_external(criteria, %{remote: true} = _params),
|
||||
do: Map.put(criteria, :external, true)
|
||||
|
||||
defp maybe_filter_external(criteria, %{remote: false} = _params),
|
||||
do: Map.put(criteria, :local, true)
|
||||
|
||||
defp maybe_filter_origin(criteria, %{origin: "local"} = _params),
|
||||
do: Map.put(criteria, :local, true)
|
||||
|
||||
defp maybe_filter_origin(criteria, %{origin: "remote"} = _params),
|
||||
do: Map.put(criteria, :external, true)
|
||||
|
||||
defp maybe_filter_active(criteria, %{active: active} = _params),
|
||||
do: Map.put(criteria, :active, active)
|
||||
|
||||
defp maybe_filter_needing_approval(criteria, %{pending: need_approval} = _params),
|
||||
do: Map.put(criteria, :need_approval, need_approval)
|
||||
|
||||
defp maybe_filter_deactivated(criteria, %{disabled: deactivated} = _params),
|
||||
do: Map.put(criteria, :deactivated, deactivated)
|
||||
|
||||
defp maybe_filter_status(criteria, %{status: "active"} = _params),
|
||||
do: Map.put(criteria, :active, true)
|
||||
|
||||
defp maybe_filter_status(criteria, %{status: "inactive"} = _params),
|
||||
do: Map.put(criteria, :active, false)
|
||||
|
||||
defp maybe_filter_status(criteria, %{status: "pending"} = _params),
|
||||
do: Map.put(criteria, :need_approval, true)
|
||||
|
||||
defp maybe_filter_status(criteria, %{status: "disabled"} = _params),
|
||||
do: Map.put(criteria, :deactivated, true)
|
||||
|
||||
defp maybe_filter_nickname(criteria, %{username: nickname} = _params),
|
||||
do: Map.put(criteria, :nickname, nickname)
|
||||
|
||||
defp maybe_filter_name(criteria, %{display_name: name} = _params),
|
||||
do: Map.put(criteria, :name, name)
|
||||
|
||||
defp maybe_filter_domain(criteria, %{by_domain: domain} = _params),
|
||||
do: Map.put(criteria, :domain, domain)
|
||||
|
||||
defp maybe_filter_email(criteria, %{email: email} = _params),
|
||||
do: Map.put(criteria, :email, email)
|
||||
|
||||
defp maybe_filter_staff(criteria, %{permissions: "staff"} = _params),
|
||||
do: Map.put(criteria, :staff, true)
|
||||
|
||||
defp maybe_filter_staff(criteria, %{staff: staff} = _params),
|
||||
do: Map.put(criteria, :staff, staff)
|
||||
|
||||
for filter_param <- @filter_params do
|
||||
defp unquote(:"maybe_filter_#{filter_param}")(criteria, _params), do: criteria
|
||||
end
|
||||
|
||||
defp associated_report(user, %{report_id: id}) do
|
||||
case Activity.get_report(id) do
|
||||
%Activity{data: %{"object" => [target_ap_id | _]}} = report
|
||||
when target_ap_id == user.ap_id ->
|
||||
{:ok, report}
|
||||
|
||||
%Activity{} ->
|
||||
{:error, "Report does not target this account"}
|
||||
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp associated_report(_user, _params), do: {:ok, nil}
|
||||
|
||||
defp resolve_report(admin, %Activity{id: id}) do
|
||||
with {:ok, activity} <- CommonAPI.update_report_state(id, "resolved"),
|
||||
report <- Activity.get_by_id_with_user_actor(activity.id) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "report_update",
|
||||
actor: admin,
|
||||
subject: activity,
|
||||
subject_actor: report.user_actor
|
||||
})
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_report(_admin, nil), do: :ok
|
||||
end
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.Admin.ReportController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
import Pleroma.Web.ControllerHelper,
|
||||
only: [
|
||||
add_link_headers: 2,
|
||||
json_response: 3
|
||||
]
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.AdminAPI.Report
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:read:reports"]} when action in [:index, :show])
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:write:reports"]} when action in [:resolve, :reopen])
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["admin:write:reports"]} when action in [:assign_to_self, :unassign]
|
||||
)
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MastodonAdmin.ReportOperation
|
||||
|
||||
def index(conn, params) do
|
||||
opts =
|
||||
%{}
|
||||
|> Map.put(:type, "Flag")
|
||||
|> Map.put(:skip_preload, true)
|
||||
|> Map.put(:preload_report_notes, true)
|
||||
|> Map.put(:total, true)
|
||||
|> restrict_state(params)
|
||||
|> restrict_actor(params)
|
||||
|> restrict_target(params)
|
||||
|
||||
reports =
|
||||
ActivityPub.fetch_activities_query([], opts)
|
||||
|> Pagination.fetch_paginated(params)
|
||||
|
||||
conn
|
||||
|> add_link_headers(reports)
|
||||
|> render("index.json", reports: reports)
|
||||
end
|
||||
|
||||
def show(conn, %{id: id}) do
|
||||
with %Activity{} = report <- Activity.get_report(id) do
|
||||
render(conn, "show.json", Report.extract_report_info(report))
|
||||
else
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def resolve(%{assigns: %{user: admin}} = conn, %{id: id}) do
|
||||
with {:ok, activity} <- CommonAPI.update_report_state(id, "resolved"),
|
||||
report <- Activity.get_by_id_with_user_actor(activity.id) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "report_update",
|
||||
actor: admin,
|
||||
subject: activity,
|
||||
subject_actor: report.user_actor
|
||||
})
|
||||
|
||||
render(conn, "show.json", Report.extract_report_info(report))
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, error} ->
|
||||
json_response(conn, :bad_request, %{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
def reopen(%{assigns: %{user: admin}} = conn, %{id: id}) do
|
||||
with {:ok, activity} <- CommonAPI.update_report_state(id, "open"),
|
||||
report <- Activity.get_by_id_with_user_actor(activity.id) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "report_update",
|
||||
actor: admin,
|
||||
subject: activity,
|
||||
subject_actor: report.user_actor
|
||||
})
|
||||
|
||||
render(conn, "show.json", Report.extract_report_info(report))
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, error} ->
|
||||
json_response(conn, :bad_request, %{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
def assign_to_self(%{assigns: %{user: admin}} = conn, %{id: id}) do
|
||||
with {:ok, activity} <- CommonAPI.assign_report_to_account(id, admin.id),
|
||||
report <- Activity.get_by_id_with_user_actor(activity.id) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "report_assigned",
|
||||
actor: admin,
|
||||
subject: activity,
|
||||
subject_actor: report.user_actor,
|
||||
assigned_account: admin.nickname
|
||||
})
|
||||
|
||||
render(conn, "show.json", Report.extract_report_info(report))
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, error} ->
|
||||
json_response(conn, :bad_request, %{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
def unassign(%{assigns: %{user: admin}} = conn, %{id: id}) do
|
||||
with {:ok, activity} <- CommonAPI.assign_report_to_account(id, nil),
|
||||
report <- Activity.get_by_id_with_user_actor(activity.id) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "report_unassigned",
|
||||
actor: admin,
|
||||
subject: activity,
|
||||
subject_actor: report.user_actor
|
||||
})
|
||||
|
||||
render(conn, "show.json", Report.extract_report_info(report))
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, error} ->
|
||||
json_response(conn, :bad_request, %{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_state(opts, %{resolved: true, unresolved: true}), do: opts
|
||||
|
||||
defp restrict_state(opts, %{resolved: true}), do: Map.put(opts, :state, "resolved")
|
||||
|
||||
defp restrict_state(opts, _params), do: Map.put(opts, :state, "open")
|
||||
|
||||
defp restrict_actor(opts, %{account_id: actor}) do
|
||||
with %User{ap_id: ap_id} <- User.get_by_id(actor) do
|
||||
Map.put(opts, :actor_id, ap_id)
|
||||
else
|
||||
_ -> Map.put(opts, :actor_id, actor)
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_actor(opts, _params), do: opts
|
||||
|
||||
defp restrict_target(opts, %{target_account_id: actor}) do
|
||||
case User.get_by_id(actor) do
|
||||
%User{ap_id: ap_id} -> Map.put(opts, :report_target_id, ap_id)
|
||||
_ -> Map.put(opts, :report_target_id, actor)
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_target(opts, _params), do: opts
|
||||
end
|
||||
64
lib/pleroma/web/mastodon_api/admin/views/account_view.ex
Normal file
64
lib/pleroma/web/mastodon_api/admin/views/account_view.ex
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.Admin.AccountView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI
|
||||
|
||||
def render("index.json", %{users: users}) do
|
||||
render_many(users, __MODULE__, "show.json", as: :user)
|
||||
end
|
||||
|
||||
def render("show.json", %{user: user}) do
|
||||
account =
|
||||
MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|
||||
|
||||
%{
|
||||
id: user.id,
|
||||
username: username_from_nickname(user.nickname),
|
||||
domain: domain_from_nickname(user.nickname),
|
||||
created_at: Utils.to_masto_date(user.inserted_at),
|
||||
email: user.email,
|
||||
ip: nil,
|
||||
ips: [],
|
||||
locale: nil,
|
||||
invite_request: user.registration_reason,
|
||||
role: role(user),
|
||||
confirmed: user.is_confirmed,
|
||||
approved: user.is_approved,
|
||||
disabled: !user.is_active,
|
||||
silenced: nil,
|
||||
suspended: nil,
|
||||
account: account
|
||||
}
|
||||
end
|
||||
|
||||
defp username_from_nickname(string) when is_binary(string) do
|
||||
hd(String.split(string, "@"))
|
||||
end
|
||||
|
||||
defp username_from_nickname(_), do: nil
|
||||
|
||||
defp domain_from_nickname(string) when is_binary(string) do
|
||||
String.split(string, "@")
|
||||
|> Enum.at(1, nil)
|
||||
end
|
||||
|
||||
defp domain_from_nickname(_), do: nil
|
||||
|
||||
defp role(%User{is_admin: true}) do
|
||||
"admin"
|
||||
end
|
||||
|
||||
defp role(%User{is_moderator: true}) do
|
||||
"moderator"
|
||||
end
|
||||
|
||||
defp role(_user) do
|
||||
nil
|
||||
end
|
||||
end
|
||||
71
lib/pleroma/web/mastodon_api/admin/views/report_view.ex
Normal file
71
lib/pleroma/web/mastodon_api/admin/views/report_view.ex
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.Admin.ReportView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Rule
|
||||
alias Pleroma.Web.AdminAPI.Report
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.Admin.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
def render("index.json", %{reports: reports}) do
|
||||
reports
|
||||
|> Enum.map(&Report.extract_report_info/1)
|
||||
|> Enum.map(&render(__MODULE__, "show.json", &1))
|
||||
end
|
||||
|
||||
def render("show.json", %{
|
||||
report: report,
|
||||
user: account,
|
||||
account: target_account,
|
||||
statuses: statuses,
|
||||
assigned_account: assigned_account
|
||||
}) do
|
||||
created_at = Utils.to_masto_date(report.data["published"])
|
||||
updated_at = Utils.to_masto_date(report.updated_at)
|
||||
action_taken = report.data["state"] != "open"
|
||||
|
||||
content =
|
||||
unless is_nil(report.data["content"]) do
|
||||
HTML.filter_tags(report.data["content"])
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
%{
|
||||
id: report.id,
|
||||
action_taken: action_taken,
|
||||
action_taken_at: if(action_taken, do: updated_at),
|
||||
category: "other",
|
||||
comment: content,
|
||||
forwarded: false,
|
||||
created_at: created_at,
|
||||
updated_at: updated_at,
|
||||
account: AccountView.render("show.json", %{user: account}),
|
||||
target_account: AccountView.render("show.json", %{user: target_account}),
|
||||
assigned_account: render_account(assigned_account),
|
||||
action_taken_by_account: nil,
|
||||
statuses:
|
||||
StatusView.render("index.json", %{
|
||||
activities: statuses,
|
||||
as: :activity
|
||||
}),
|
||||
rules: rules(report.data["rules"])
|
||||
}
|
||||
end
|
||||
|
||||
defp render_account(nil), do: nil
|
||||
defp render_account(account), do: AccountView.render("show.json", %{user: account})
|
||||
|
||||
defp rules(nil), do: []
|
||||
|
||||
defp rules(rule_ids) do
|
||||
rule_ids
|
||||
|> Rule.get()
|
||||
|> Enum.map(&%{id: to_string(&1.id), text: &1.text})
|
||||
end
|
||||
end
|
||||
|
|
@ -348,6 +348,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
params
|
||||
|> Map.delete(:tagged)
|
||||
|> Map.put(:tag, params[:tagged])
|
||||
|> Map.put(:restrict_unauthenticated, true)
|
||||
|
||||
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
|
||||
|
||||
|
|
|
|||
|
|
@ -101,13 +101,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
|> Enum.filter(& &1)
|
||||
|> Enum.uniq_by(& &1[:id])
|
||||
|
||||
actors =
|
||||
accounts =
|
||||
notification_groups
|
||||
|> List.flatten()
|
||||
|> notification_actors()
|
||||
|> notification_accounts()
|
||||
|
||||
%{
|
||||
accounts: AccountView.render("index.json", %{users: actors, for: reading_user}),
|
||||
accounts: AccountView.render("index.json", %{users: accounts, for: reading_user}),
|
||||
statuses: statuses,
|
||||
notification_groups:
|
||||
Enum.map(
|
||||
|
|
@ -191,7 +191,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
|
||||
defp render_group(
|
||||
[%Notification{} = notification | _] = notifications,
|
||||
_reading_user,
|
||||
reading_user,
|
||||
grouped_types,
|
||||
notification_group_counts,
|
||||
notification_group_bounds,
|
||||
|
|
@ -237,13 +237,49 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
response
|
||||
end
|
||||
|
||||
if status_activity do
|
||||
Map.put(response, :status_id, to_string(status_activity.id))
|
||||
else
|
||||
response
|
||||
response =
|
||||
if status_activity do
|
||||
Map.put(response, :status_id, to_string(status_activity.id))
|
||||
else
|
||||
response
|
||||
end
|
||||
|
||||
put_group_details(response, notification, reading_user)
|
||||
end
|
||||
|
||||
defp put_group_details(response, %Notification{type: "move", activity: activity}, _reading_user) do
|
||||
case User.get_cached_by_ap_id(activity.data["target"]) do
|
||||
%User{id: id} -> Map.put(response, :target_id, to_string(id))
|
||||
_ -> response
|
||||
end
|
||||
end
|
||||
|
||||
defp put_group_details(
|
||||
response,
|
||||
%Notification{type: "pleroma:emoji_reaction", activity: activity},
|
||||
_reading_user
|
||||
) do
|
||||
put_emoji(response, activity)
|
||||
end
|
||||
|
||||
defp put_group_details(
|
||||
response,
|
||||
%Notification{type: "pleroma:chat_mention", activity: activity},
|
||||
reading_user
|
||||
) do
|
||||
put_chat_message(response, activity, reading_user, %{})
|
||||
end
|
||||
|
||||
defp put_group_details(
|
||||
response,
|
||||
%Notification{type: "pleroma:report", activity: activity},
|
||||
_reading_user
|
||||
) do
|
||||
put_report(response, activity)
|
||||
end
|
||||
|
||||
defp put_group_details(response, _notification, _reading_user), do: response
|
||||
|
||||
defp status_activity_for_group([%Notification{} = notification | _], grouped_types) do
|
||||
status_activity_for(notification, grouped_types)
|
||||
end
|
||||
|
|
@ -287,6 +323,19 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
|> Enum.uniq_by(& &1.id)
|
||||
end
|
||||
|
||||
defp notification_accounts(notifications) do
|
||||
move_targets =
|
||||
notifications
|
||||
|> Enum.filter(&(&1.type == "move"))
|
||||
|> Enum.map(&User.get_cached_by_ap_id(&1.activity.data["target"]))
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
notifications
|
||||
|> notification_actors()
|
||||
|> Kernel.++(move_targets)
|
||||
|> Enum.uniq_by(& &1.id)
|
||||
end
|
||||
|
||||
defp put_emoji(response, activity) do
|
||||
response
|
||||
|> Map.put(:emoji, activity.data["content"])
|
||||
|
|
|
|||
|
|
@ -587,14 +587,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
video_url = proxied_url(rich_media["video"], page_url_data)
|
||||
|
||||
%{
|
||||
type: "link",
|
||||
provider_name: page_url_data.host,
|
||||
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
|
||||
type: card_type(rich_media["type"]),
|
||||
provider_name: rich_media["provider_name"] || page_url_data.host,
|
||||
provider_url:
|
||||
rich_media["provider_url"] || page_url_data.scheme <> "://" <> page_url_data.host,
|
||||
url: page_url,
|
||||
image: image_url,
|
||||
image_description: rich_media["image:alt"] || "",
|
||||
title: rich_media["title"] || "",
|
||||
description: rich_media["description"] || "",
|
||||
language: nil,
|
||||
authors: [],
|
||||
author_name: rich_media["author_name"] || "",
|
||||
author_url: rich_media["author_url"] || "",
|
||||
html: rich_media["html"] || "",
|
||||
width: card_dimension(rich_media["width"]),
|
||||
height: card_dimension(rich_media["height"]),
|
||||
embed_url: rich_media["embed_url"] || "",
|
||||
blurhash: nil,
|
||||
published_at: nil,
|
||||
pleroma: %{
|
||||
opengraph:
|
||||
rich_media
|
||||
|
|
@ -620,7 +631,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
String.contains?(media_type, "image") -> "image"
|
||||
String.contains?(media_type, "video") -> "video"
|
||||
String.contains?(media_type, "audio") -> "audio"
|
||||
attachment["type"] == "Image" -> "image"
|
||||
true -> "unknown"
|
||||
end
|
||||
|
||||
|
|
@ -699,6 +709,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
%{content: content, detected_source_language: detected_source_language, provider: provider}
|
||||
end
|
||||
|
||||
defp card_type(type) when type in ["link", "photo", "video", "rich"], do: type
|
||||
defp card_type(_), do: "link"
|
||||
|
||||
defp card_dimension(value) when is_integer(value) and value >= 0, do: value
|
||||
|
||||
defp card_dimension(value) when is_binary(value) do
|
||||
case Integer.parse(value) do
|
||||
{dimension, ""} when dimension >= 0 -> dimension
|
||||
_ -> 0
|
||||
end
|
||||
end
|
||||
|
||||
defp card_dimension(_), do: 0
|
||||
|
||||
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
|
|
|
|||
|
|
@ -208,7 +208,6 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
|||
|
||||
defp media_proxy_opts do
|
||||
Config.get([:media_proxy, :proxy_opts], [])
|
||||
|> Keyword.put_new(:sniff_content_type, true)
|
||||
end
|
||||
|
||||
defp sandbox(conn, _params) do
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.OAuth.App do
|
|||
import Ecto.Query
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
|
@ -191,15 +192,26 @@ defmodule Pleroma.Web.OAuth.App do
|
|||
|
||||
@spec remove_orphans(pos_integer()) :: :ok
|
||||
def remove_orphans(limit \\ 100) do
|
||||
fifteen_mins_ago = DateTime.add(DateTime.utc_now(), -900, :second)
|
||||
fifteen_mins_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -900)
|
||||
|
||||
Repo.transaction(fn ->
|
||||
app_ids =
|
||||
from(a in __MODULE__,
|
||||
where: is_nil(a.user_id) and a.inserted_at < ^fifteen_mins_ago,
|
||||
limit: ^limit,
|
||||
lock: "FOR UPDATE SKIP LOCKED",
|
||||
select: a.id
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
# Bulk deletes bypass the schema's association cleanup.
|
||||
from(a in Authorization, where: a.app_id in ^app_ids) |> Repo.delete_all()
|
||||
from(t in Token, where: t.app_id in ^app_ids) |> Repo.delete_all()
|
||||
|
||||
from(a in __MODULE__,
|
||||
where: is_nil(a.user_id) and a.inserted_at < ^fifteen_mins_ago,
|
||||
limit: ^limit
|
||||
where: a.id in ^app_ids and is_nil(a.user_id) and a.inserted_at < ^fifteen_mins_ago
|
||||
)
|
||||
|> Repo.all()
|
||||
|> Enum.each(&Repo.delete(&1))
|
||||
|> Repo.delete_all()
|
||||
end)
|
||||
|
||||
:ok
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlug do
|
|||
import Phoenix.Controller, only: [json: 2]
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Config
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
|
@ -27,9 +29,11 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlug do
|
|||
def call(conn, _), do: conn
|
||||
|
||||
def process_request(conn, key) do
|
||||
case @cachex.get(:idempotency_cache, key) do
|
||||
cache_key = {conn.method, conn.request_path, actor_key(conn), key}
|
||||
|
||||
case @cachex.get(:idempotency_cache, cache_key) do
|
||||
{:ok, nil} ->
|
||||
cache_resposnse(conn, key)
|
||||
cache_resposnse(conn, key, cache_key)
|
||||
|
||||
{:ok, record} ->
|
||||
send_cached(conn, key, record)
|
||||
|
|
@ -39,18 +43,48 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlug do
|
|||
end
|
||||
end
|
||||
|
||||
defp cache_resposnse(conn, key) do
|
||||
register_before_send(conn, fn conn ->
|
||||
[request_id] = get_resp_header(conn, "x-request-id")
|
||||
content_type = get_content_type(conn)
|
||||
defp cache_resposnse(conn, key, cache_key) do
|
||||
register_before_send(conn, &maybe_cache_response(&1, key, cache_key))
|
||||
end
|
||||
|
||||
record = {request_id, content_type, conn.status, conn.resp_body}
|
||||
{:ok, _} = @cachex.put(:idempotency_cache, key, record)
|
||||
defp maybe_cache_response(
|
||||
%{private: %{skip_idempotency_cache: true}} = conn,
|
||||
_key,
|
||||
_cache_key
|
||||
),
|
||||
do: conn
|
||||
|
||||
conn
|
||||
|> put_resp_header("idempotency-key", key)
|
||||
|> put_resp_header("x-original-request-id", request_id)
|
||||
end)
|
||||
defp maybe_cache_response(conn, key, cache_key) do
|
||||
[request_id] = get_resp_header(conn, "x-request-id")
|
||||
content_type = get_content_type(conn)
|
||||
|
||||
record = {request_id, content_type, conn.status, conn.resp_body}
|
||||
{:ok, _} = @cachex.put(:idempotency_cache, cache_key, record)
|
||||
|
||||
conn
|
||||
|> put_resp_header("idempotency-key", key)
|
||||
|> put_resp_header("x-original-request-id", request_id)
|
||||
end
|
||||
|
||||
defp actor_key(%{assigns: assigns}) do
|
||||
{principal_key(assigns), token_key(assigns[:token]), staff_privileges()}
|
||||
end
|
||||
|
||||
defp principal_key(%{user: %{id: id} = user}) do
|
||||
{:user, id, Map.get(user, :is_admin), Map.get(user, :is_moderator)}
|
||||
end
|
||||
|
||||
defp principal_key(%{app: %{id: id}}), do: {:app, id}
|
||||
defp principal_key(_assigns), do: nil
|
||||
|
||||
defp token_key(%{id: id} = token), do: {id, token |> Map.get(:scopes, []) |> Enum.sort()}
|
||||
defp token_key(_token), do: nil
|
||||
|
||||
defp staff_privileges do
|
||||
{
|
||||
Config.get([:instance, :admin_privileges], []) |> Enum.sort(),
|
||||
Config.get([:instance, :moderator_privileges], []) |> Enum.sort()
|
||||
}
|
||||
end
|
||||
|
||||
defp send_cached(conn, key, record) do
|
||||
|
|
|
|||
|
|
@ -137,6 +137,12 @@ defmodule Pleroma.Web.Router do
|
|||
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_activation_state)
|
||||
end
|
||||
|
||||
pipeline :require_privileged_role_account_actions do
|
||||
plug(:admin_api)
|
||||
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_activation_state)
|
||||
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :reports_manage_reports)
|
||||
end
|
||||
|
||||
pipeline :require_privileged_role_users_manage_invites do
|
||||
plug(:admin_api)
|
||||
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_invites)
|
||||
|
|
@ -316,6 +322,15 @@ defmodule Pleroma.Web.Router do
|
|||
post("/rules", RuleController, :create)
|
||||
patch("/rules/:id", RuleController, :update)
|
||||
delete("/rules/:id", RuleController, :delete)
|
||||
|
||||
get("/webhooks", WebhookController, :index)
|
||||
get("/webhooks/:id", WebhookController, :show)
|
||||
post("/webhooks", WebhookController, :create)
|
||||
patch("/webhooks/:id", WebhookController, :update)
|
||||
delete("/webhooks/:id", WebhookController, :delete)
|
||||
post("/webhooks/:id/enable", WebhookController, :enable)
|
||||
post("/webhooks/:id/disable", WebhookController, :disable)
|
||||
post("/webhooks/:id/rotate_secret", WebhookController, :rotate_secret)
|
||||
end
|
||||
|
||||
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
|
||||
|
|
@ -424,6 +439,60 @@ defmodule Pleroma.Web.Router do
|
|||
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
|
||||
end
|
||||
|
||||
# Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
|
||||
scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do
|
||||
pipe_through([:require_privileged_role_users_read])
|
||||
|
||||
get("/accounts", AccountController, :index)
|
||||
get("/accounts/:id", AccountController, :show)
|
||||
end
|
||||
|
||||
# Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
|
||||
scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do
|
||||
pipe_through(:require_privileged_role_users_delete)
|
||||
|
||||
delete("/accounts/:id", AccountController, :delete)
|
||||
end
|
||||
|
||||
# Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
|
||||
scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do
|
||||
pipe_through([:require_privileged_role_users_manage_activation_state])
|
||||
|
||||
post("/accounts/:id/enable", AccountController, :enable)
|
||||
end
|
||||
|
||||
scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do
|
||||
pipe_through([:require_privileged_role_account_actions])
|
||||
|
||||
post("/accounts/:id/action", AccountController, :account_action)
|
||||
end
|
||||
|
||||
# Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
|
||||
scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do
|
||||
pipe_through([:require_privileged_role_users_manage_invites])
|
||||
post("/accounts/:id/approve", AccountController, :approve)
|
||||
post("/accounts/:id/reject", AccountController, :reject)
|
||||
end
|
||||
|
||||
# Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
|
||||
scope "/api/v1/admin", Pleroma.Web.MastodonAPI.Admin do
|
||||
pipe_through([:require_privileged_role_reports_manage_reports])
|
||||
|
||||
get("/reports", ReportController, :index)
|
||||
get("/reports/:id", ReportController, :show)
|
||||
post("/reports/:id/resolve", ReportController, :resolve)
|
||||
post("/reports/:id/reopen", ReportController, :reopen)
|
||||
post("/reports/:id/assign_to_self", ReportController, :assign_to_self)
|
||||
post("/reports/:id/unassign", ReportController, :unassign)
|
||||
end
|
||||
|
||||
# Mastodon AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
|
||||
scope "/api/v2/admin", Pleroma.Web.MastodonAPI.Admin do
|
||||
pipe_through([:require_privileged_role_users_read])
|
||||
|
||||
get("/accounts", AccountController, :index2)
|
||||
end
|
||||
|
||||
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
|
||||
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
|
||||
pipe_through(:require_privileged_role_emoji_manage_emoji)
|
||||
|
|
|
|||
98
lib/pleroma/webhook.ex
Normal file
98
lib/pleroma/webhook.ex
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Webhook do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Repo
|
||||
|
||||
@event_types [:"account.created", :"report.created"]
|
||||
|
||||
schema "webhooks" do
|
||||
field(:url, ObjectValidators.Uri)
|
||||
field(:events, {:array, Ecto.Enum}, values: @event_types, default: [])
|
||||
field(:secret, :string, default: "")
|
||||
field(:enabled, :boolean, default: true)
|
||||
field(:internal, :boolean, default: false)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def get(id), do: Repo.get(__MODULE__, id)
|
||||
|
||||
def get_by_type(type) do
|
||||
__MODULE__
|
||||
|> where([w], ^type in w.events)
|
||||
|> where([w], w.enabled == true)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def changeset(%__MODULE__{} = webhook, params) do
|
||||
webhook
|
||||
|> cast(params, [:url, :events, :enabled, :internal])
|
||||
|> validate_required([:url, :events])
|
||||
|> validate_length(:url, max: 255)
|
||||
|> validate_events_present()
|
||||
|> unique_constraint(:url)
|
||||
|> put_secret()
|
||||
end
|
||||
|
||||
def update_changeset(%__MODULE__{} = webhook, params \\ %{}) do
|
||||
webhook
|
||||
|> cast(params, [:url, :events, :enabled, :internal])
|
||||
|> validate_length(:url, max: 255)
|
||||
|> validate_events_present()
|
||||
|> unique_constraint(:url)
|
||||
end
|
||||
|
||||
def create(params) do
|
||||
%__MODULE__{}
|
||||
|> changeset(params)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def update(%__MODULE__{} = webhook, params) do
|
||||
webhook
|
||||
|> update_changeset(params)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def delete(webhook), do: webhook |> Repo.delete()
|
||||
|
||||
def rotate_secret(%__MODULE__{} = webhook) do
|
||||
webhook
|
||||
|> cast(%{}, [])
|
||||
|> put_secret()
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def set_enabled(%__MODULE__{} = webhook, enabled) do
|
||||
webhook
|
||||
|> cast(%{enabled: enabled}, [:enabled])
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
defp validate_events_present(changeset) do
|
||||
cond do
|
||||
Keyword.has_key?(changeset.errors, :events) -> changeset
|
||||
get_field(changeset, :internal) -> changeset
|
||||
match?([_ | _], get_field(changeset, :events)) -> changeset
|
||||
true -> add_error(changeset, :events, "can't be blank")
|
||||
end
|
||||
end
|
||||
|
||||
defp put_secret(changeset) do
|
||||
changeset
|
||||
|> put_change(:secret, generate_secret())
|
||||
end
|
||||
|
||||
defp generate_secret do
|
||||
Base.encode16(:crypto.strong_rand_bytes(20))
|
||||
|> String.downcase()
|
||||
end
|
||||
end
|
||||
94
lib/pleroma/webhook/notify.ex
Normal file
94
lib/pleroma/webhook/notify.ex
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Webhook.Notify do
|
||||
alias Phoenix.View
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.AdminAPI.Report
|
||||
alias Pleroma.Webhook
|
||||
alias Pleroma.Workers.WebhookWorker
|
||||
|
||||
def trigger_webhooks(%Activity{} = activity, :"report.created" = type) do
|
||||
webhooks = Webhook.get_by_type(type)
|
||||
|
||||
webhooks
|
||||
|> Enum.map(fn webhook ->
|
||||
WebhookWorker.new(%{
|
||||
"op" => "notify",
|
||||
"type" => type,
|
||||
"webhook_id" => webhook.id,
|
||||
"activity_id" => activity.id
|
||||
})
|
||||
end)
|
||||
|> Oban.insert_all()
|
||||
end
|
||||
|
||||
def trigger_webhooks(%User{} = user, :"account.created" = type) do
|
||||
webhooks = Webhook.get_by_type(type)
|
||||
|
||||
webhooks
|
||||
|> Enum.map(fn webhook ->
|
||||
WebhookWorker.new(%{
|
||||
"op" => "notify",
|
||||
"type" => type,
|
||||
"webhook_id" => webhook.id,
|
||||
"user_id" => user.id
|
||||
})
|
||||
end)
|
||||
|> Oban.insert_all()
|
||||
end
|
||||
|
||||
def report_created(%Webhook{} = webhook, %Activity{} = report) do
|
||||
object =
|
||||
View.render(
|
||||
Pleroma.Web.MastodonAPI.Admin.ReportView,
|
||||
"show.json",
|
||||
Report.extract_report_info(report)
|
||||
)
|
||||
|
||||
deliver(webhook, object, :"report.created")
|
||||
end
|
||||
|
||||
def account_created(%Webhook{} = webhook, %User{} = user) do
|
||||
object =
|
||||
View.render(
|
||||
Pleroma.Web.MastodonAPI.Admin.AccountView,
|
||||
"show.json",
|
||||
user: user
|
||||
)
|
||||
|
||||
deliver(webhook, object, :"account.created")
|
||||
end
|
||||
|
||||
defp deliver(%Webhook{url: url, secret: secret}, object, type) do
|
||||
body =
|
||||
View.render_to_string(Pleroma.Web.AdminAPI.WebhookView, "event.json",
|
||||
type: type,
|
||||
object: object
|
||||
)
|
||||
|
||||
headers = [
|
||||
{"Content-Type", "application/json"},
|
||||
{"X-Hub-Signature", "sha256=#{signature(body, secret)}"}
|
||||
]
|
||||
|
||||
ConcurrentLimiter.limit(__MODULE__, fn ->
|
||||
case Pleroma.HTTP.post(url, body, headers) do
|
||||
{:ok, %Tesla.Env{status: status}} when status in 200..299 ->
|
||||
:ok
|
||||
|
||||
{:ok, %Tesla.Env{status: status}} ->
|
||||
{:error, {:http_status, status}}
|
||||
|
||||
{:error, _reason} = error ->
|
||||
error
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp signature(body, secret) do
|
||||
:crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16(case: :lower)
|
||||
end
|
||||
end
|
||||
50
lib/pleroma/workers/webhook_worker.ex
Normal file
50
lib/pleroma/workers/webhook_worker.ex
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.WebhookWorker do
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Webhook
|
||||
alias Pleroma.Webhook.Notify
|
||||
|
||||
use Oban.Worker, queue: :background
|
||||
|
||||
@impl true
|
||||
def perform(%Job{
|
||||
args: %{
|
||||
"op" => "notify",
|
||||
"type" => "report.created",
|
||||
"webhook_id" => webhook_id,
|
||||
"activity_id" => report_id
|
||||
}
|
||||
}) do
|
||||
with %Webhook{enabled: true} = webhook <- Webhook.get(webhook_id),
|
||||
%Activity{} = report <- Activity.get_by_id(report_id) do
|
||||
Notify.report_created(webhook, report)
|
||||
else
|
||||
%Webhook{enabled: false} -> {:cancel, :disabled}
|
||||
nil -> {:cancel, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def perform(%Job{
|
||||
args: %{
|
||||
"op" => "notify",
|
||||
"type" => "account.created",
|
||||
"webhook_id" => webhook_id,
|
||||
"user_id" => user_id
|
||||
}
|
||||
}) do
|
||||
with %Webhook{enabled: true} = webhook <- Webhook.get(webhook_id),
|
||||
%User{} = user <- User.get_by_id(user_id) do
|
||||
Notify.account_created(webhook, user)
|
||||
else
|
||||
%Webhook{enabled: false} -> {:cancel, :disabled}
|
||||
nil -> {:cancel, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def timeout(_job), do: :timer.seconds(10)
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue