Merge branch 'develop' into webhooks
This commit is contained in:
commit
133fc64472
432 changed files with 13238 additions and 3502 deletions
|
|
@ -234,6 +234,61 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
end)
|
||||
end
|
||||
|
||||
# Removes non-whitelisted configuration sections
|
||||
def run(["filter_whitelisted" | rest]) do
|
||||
{options, [], []} =
|
||||
OptionParser.parse(
|
||||
rest,
|
||||
strict: [force: :boolean],
|
||||
aliases: [f: :force]
|
||||
)
|
||||
|
||||
force = Keyword.get(options, :force, false)
|
||||
|
||||
start_pleroma()
|
||||
|
||||
whitelisted_configs = Pleroma.Config.get(:database_config_whitelist)
|
||||
|
||||
if whitelisted_configs in [nil, false] do
|
||||
shell_error("No unwanted settings in ConfigDB. No changes made.")
|
||||
else
|
||||
whitelisted_groups =
|
||||
whitelisted_configs
|
||||
|> Enum.filter(fn
|
||||
{_group} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> Enum.map(fn {group} -> group end)
|
||||
|
||||
whitelisted_keys =
|
||||
whitelisted_configs
|
||||
|> Enum.filter(fn
|
||||
{_group, _key} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|
||||
filtered =
|
||||
from(c in ConfigDB)
|
||||
|> Repo.all()
|
||||
|> Enum.filter(¬_whitelisted?(&1, whitelisted_groups, whitelisted_keys))
|
||||
|
||||
if not Enum.empty?(filtered) do
|
||||
shell_info("The following settings will be removed from ConfigDB:\n")
|
||||
Enum.each(filtered, &dump(&1))
|
||||
|
||||
if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
|
||||
filtered_ids = Enum.map(filtered, fn %{id: id} -> id end)
|
||||
|
||||
Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids))
|
||||
else
|
||||
shell_error("No changes made.")
|
||||
end
|
||||
else
|
||||
shell_error("No unwanted settings in ConfigDB. No changes made.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec migrate_to_db(Path.t() | nil) :: any()
|
||||
def migrate_to_db(file_path \\ nil) do
|
||||
with :ok <- Pleroma.Config.DeprecationWarnings.warn() do
|
||||
|
|
@ -330,7 +385,13 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
|> Enum.each(&write_and_delete(&1, file, opts[:delete]))
|
||||
|
||||
:ok = File.close(file)
|
||||
System.cmd("mix", ["format", path])
|
||||
|
||||
# Ensure `mix format` runs in the same env as the current task and doesn't
|
||||
# emit config-time stderr noise (e.g. dev secret warnings) into `mix test`.
|
||||
System.cmd("mix", ["format", path],
|
||||
env: [{"MIX_ENV", to_string(Mix.env())}],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
end
|
||||
|
||||
defp config_header, do: "import Config\r\n\r\n"
|
||||
|
|
@ -428,4 +489,9 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
|
||||
Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
|
||||
end
|
||||
|
||||
defp not_whitelisted?(%{group: group, key: key}, whitelisted_groups, whitelisted_keys) do
|
||||
not Enum.member?(whitelisted_groups, group) and
|
||||
not Enum.member?(whitelisted_keys, {group, key})
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -226,7 +226,12 @@ defmodule Mix.Tasks.Pleroma.Database do
|
|||
DELETE FROM hashtags AS ht
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM hashtags_objects hto
|
||||
WHERE ht.id = hto.hashtag_id)
|
||||
WHERE ht.id = hto.hashtag_id
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_follows_hashtag ufh
|
||||
WHERE ht.id = ufh.hashtag_id
|
||||
)
|
||||
"""
|
||||
|> Repo.query()
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.OpenapiSpec do
|
|||
else
|
||||
{_, errors} ->
|
||||
IO.puts(IO.ANSI.format([:red, :bright, "Spec check failed, errors:"]))
|
||||
Enum.map(errors, &IO.puts/1)
|
||||
Enum.each(errors, &IO.puts/1)
|
||||
|
||||
raise "Spec check failed"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
|
|||
query,
|
||||
timeout: :infinity
|
||||
)
|
||||
|> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1)
|
||||
|> Stream.map(&Pleroma.Search.object_to_search_data/1)
|
||||
|> Stream.filter(fn o -> not is_nil(o) end)
|
||||
|> Stream.chunk_every(chunk_size)
|
||||
|> Stream.transform(0, fn objects, acc ->
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ defmodule Pleroma.Activity.HTML do
|
|||
|
||||
def invalidate_cache_for(activity_id) do
|
||||
keys = get_cache_keys_for(activity_id)
|
||||
Enum.map(keys, &@cachex.del(:scrubber_cache, &1))
|
||||
Enum.each(keys, &@cachex.del(:scrubber_cache, &1))
|
||||
@cachex.del(:scrubber_management_cache, activity_id)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule Pleroma.Activity.Queries do
|
|||
Contains queries for Activity.
|
||||
"""
|
||||
|
||||
import Ecto.Query, only: [from: 2, where: 3]
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
@type query :: Ecto.Queryable.t() | Pleroma.Activity.t()
|
||||
|
||||
|
|
@ -70,22 +70,6 @@ defmodule Pleroma.Activity.Queries do
|
|||
)
|
||||
end
|
||||
|
||||
@spec by_object_in_reply_to_id(query, String.t(), keyword()) :: query
|
||||
def by_object_in_reply_to_id(query, in_reply_to_id, opts \\ []) do
|
||||
query =
|
||||
if opts[:skip_preloading] do
|
||||
Activity.with_joined_object(query)
|
||||
else
|
||||
Activity.with_preloaded_object(query)
|
||||
end
|
||||
|
||||
where(
|
||||
query,
|
||||
[activity, object: o],
|
||||
fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(in_reply_to_id))
|
||||
)
|
||||
end
|
||||
|
||||
@spec by_type(query, String.t()) :: query
|
||||
def by_type(query \\ Activity, activity_type) do
|
||||
from(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ defmodule Pleroma.Bookmark do
|
|||
schema "bookmarks" do
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.Type)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
|
@ -38,7 +38,7 @@ defmodule Pleroma.Bookmark do
|
|||
|> validate_required([:user_id, :activity_id])
|
||||
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
|
||||
|> Repo.insert(
|
||||
on_conflict: [set: [folder_id: folder_id]],
|
||||
on_conflict: [set: [folder_id: folder_id, updated_at: NaiveDateTime.utc_now()]],
|
||||
conflict_target: [:user_id, :activity_id]
|
||||
)
|
||||
end
|
||||
|
|
@ -76,11 +76,4 @@ defmodule Pleroma.Bookmark do
|
|||
|> Repo.one()
|
||||
|> Repo.delete()
|
||||
end
|
||||
|
||||
def set_folder(bookmark, folder_id) do
|
||||
bookmark
|
||||
|> cast(%{folder_id: folder_id}, [:folder_id])
|
||||
|> validate_required([:folder_id])
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ defmodule Pleroma.BookmarkFolder do
|
|||
alias Pleroma.User
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
|
||||
@primary_key {:id, FlakeId.Ecto.Type, autogenerate: true}
|
||||
|
||||
schema "bookmark_folders" do
|
||||
field(:name, :string)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ defmodule Pleroma.Chat do
|
|||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
field(:recipient, :string)
|
||||
|
||||
field(:pinned, :boolean)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
|
|
@ -94,4 +96,16 @@ defmodule Pleroma.Chat do
|
|||
order_by: [desc: c.updated_at]
|
||||
)
|
||||
end
|
||||
|
||||
def pin(%__MODULE__{} = chat) do
|
||||
chat
|
||||
|> cast(%{pinned: true}, [:pinned])
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def unpin(%__MODULE__{} = chat) do
|
||||
chat
|
||||
|> cast(%{pinned: false}, [:pinned])
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.ConfigDB do
|
|||
import Pleroma.Web.Gettext
|
||||
|
||||
alias __MODULE__
|
||||
alias Pleroma.EctoType.Config.RateLimit
|
||||
alias Pleroma.Repo
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
|
@ -60,8 +61,59 @@ defmodule Pleroma.ConfigDB do
|
|||
|> cast(params, [:key, :group, :value])
|
||||
|> validate_required([:key, :group, :value])
|
||||
|> unique_constraint(:key, name: :config_group_key_index)
|
||||
|> validate_rate_limit()
|
||||
end
|
||||
|
||||
defp validate_rate_limit(changeset) do
|
||||
group = get_field(changeset, :group)
|
||||
key = get_field(changeset, :key)
|
||||
|
||||
if group == :pleroma and key == :rate_limit do
|
||||
value = get_field(changeset, :value)
|
||||
|
||||
case normalize_rate_limit(value) do
|
||||
{:ok, normalized_value} ->
|
||||
put_change(changeset, :value, normalized_value)
|
||||
|
||||
{:error, {limiter_name, reason}} ->
|
||||
add_error(
|
||||
changeset,
|
||||
:value,
|
||||
"invalid :rate_limit value for #{inspect(limiter_name)}: #{reason}"
|
||||
)
|
||||
end
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_rate_limit(nil), do: {:ok, nil}
|
||||
|
||||
defp normalize_rate_limit(%{} = value), do: normalize_rate_limit(Map.to_list(value))
|
||||
|
||||
defp normalize_rate_limit(value) when is_list(value) do
|
||||
if Keyword.keyword?(value) do
|
||||
value
|
||||
|> Enum.reduce_while({:ok, []}, fn {limiter_name, limiter_value}, {:ok, acc} ->
|
||||
case RateLimit.cast_with_error(limiter_value) do
|
||||
{:ok, normalized_limiter_value} ->
|
||||
{:cont, {:ok, [{limiter_name, normalized_limiter_value} | acc]}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:halt, {:error, {limiter_name, reason}}}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:ok, acc} -> {:ok, Enum.reverse(acc)}
|
||||
{:error, _} = error -> error
|
||||
end
|
||||
else
|
||||
{:error, {:rate_limit, "must be a keyword list"}}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_rate_limit(_), do: {:error, {:rate_limit, "must be a keyword list"}}
|
||||
|
||||
defp create(params) do
|
||||
%ConfigDB{}
|
||||
|> changeset(params)
|
||||
|
|
|
|||
|
|
@ -21,13 +21,15 @@ defmodule Pleroma.Constants do
|
|||
"pleroma_internal",
|
||||
"generator",
|
||||
"rules",
|
||||
"language"
|
||||
"language",
|
||||
"voters",
|
||||
"assigned_account"
|
||||
]
|
||||
)
|
||||
|
||||
const(static_only_files,
|
||||
do:
|
||||
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
|
||||
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js schemas doc embed.js embed.css)
|
||||
)
|
||||
|
||||
const(status_updatable_fields,
|
||||
|
|
@ -132,6 +134,13 @@ defmodule Pleroma.Constants do
|
|||
do: ~r/^[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+\/[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+(; .*)?$/
|
||||
)
|
||||
|
||||
# List of allowed chars in the path segment of a URI
|
||||
# unreserved, sub-delims, ":", "@" and "/" allowed as the separator in path
|
||||
# https://datatracker.ietf.org/doc/html/rfc3986
|
||||
const(uri_path_allowed_reserved_chars,
|
||||
do: ~c"!$&'()*+,;=/:@"
|
||||
)
|
||||
|
||||
const(upload_object_types, do: ["Document", "Image"])
|
||||
|
||||
const(activity_json_canonical_mime_type,
|
||||
|
|
|
|||
71
lib/pleroma/ecto_type/config/rate_limit.ex
Normal file
71
lib/pleroma/ecto_type/config/rate_limit.ex
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.Config.RateLimit do
|
||||
@moduledoc false
|
||||
|
||||
use Ecto.Type
|
||||
|
||||
@type t ::
|
||||
nil
|
||||
| {non_neg_integer(), non_neg_integer()}
|
||||
| [{non_neg_integer(), non_neg_integer()}]
|
||||
|
||||
@impl true
|
||||
def type, do: :term
|
||||
|
||||
@impl true
|
||||
def cast(value) do
|
||||
case cast_with_error(value) do
|
||||
{:ok, normalized} -> {:ok, normalized}
|
||||
{:error, _reason} -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def load(value), do: cast(value)
|
||||
|
||||
@impl true
|
||||
def dump(value), do: cast(value)
|
||||
|
||||
@spec cast_with_error(term()) :: {:ok, t()} | {:error, String.t()}
|
||||
def cast_with_error(nil), do: {:ok, nil}
|
||||
|
||||
def cast_with_error({scale, limit}) do
|
||||
with {:ok, scale} <- parse_integer(scale, "scale"),
|
||||
{:ok, limit} <- parse_integer(limit, "limit"),
|
||||
true <- scale >= 1 and limit >= 1 do
|
||||
{:ok, {scale, limit}}
|
||||
else
|
||||
false -> {:error, "scale and limit must be >= 1"}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def cast_with_error([{_, _} = unauth, {_, _} = auth]) do
|
||||
with {:ok, unauth} <- cast_with_error(unauth),
|
||||
{:ok, auth} <- cast_with_error(auth) do
|
||||
{:ok, [unauth, auth]}
|
||||
else
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def cast_with_error(_),
|
||||
do:
|
||||
{:error, "must be a {scale, limit} tuple, a [{scale, limit}, {scale, limit}] list, or nil"}
|
||||
|
||||
defp parse_integer(value, _label) when is_integer(value), do: {:ok, value}
|
||||
|
||||
defp parse_integer(value, label) when is_binary(value) do
|
||||
value = String.trim(value)
|
||||
|
||||
case Integer.parse(value) do
|
||||
{number, ""} -> {:ok, number}
|
||||
_ -> {:error, "#{label} must be an integer"}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_integer(_value, label), do: {:error, "#{label} must be an integer"}
|
||||
end
|
||||
|
|
@ -11,6 +11,8 @@ defmodule Pleroma.Emoji do
|
|||
|
||||
alias Pleroma.Emoji.Combinations
|
||||
alias Pleroma.Emoji.Loader
|
||||
alias Pleroma.Utils.URIEncoding
|
||||
alias Pleroma.Web.Endpoint
|
||||
|
||||
require Logger
|
||||
|
||||
|
|
@ -189,6 +191,34 @@ defmodule Pleroma.Emoji do
|
|||
|
||||
def emoji_url(_), do: nil
|
||||
|
||||
@spec local_url(String.t() | nil) :: String.t() | nil
|
||||
def local_url(nil), do: nil
|
||||
|
||||
def local_url("http" <> _ = url) do
|
||||
URIEncoding.encode_url(url)
|
||||
end
|
||||
|
||||
def local_url("/" <> _ = path) do
|
||||
path = URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true)
|
||||
Endpoint.url() <> path
|
||||
end
|
||||
|
||||
def local_url(path) when is_binary(path) do
|
||||
local_url("/" <> path)
|
||||
end
|
||||
|
||||
def build_emoji_tag({name, url}) do
|
||||
url = URIEncoding.encode_url(url)
|
||||
|
||||
%{
|
||||
"icon" => %{"url" => "#{url}", "type" => "Image"},
|
||||
"name" => ":" <> name <> ":",
|
||||
"type" => "Emoji",
|
||||
"updated" => "1970-01-01T00:00:00Z",
|
||||
"id" => url
|
||||
}
|
||||
end
|
||||
|
||||
def emoji_name_with_instance(name, url) do
|
||||
url = url |> URI.parse() |> Map.get(:host)
|
||||
"#{name}@#{url}"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
defmodule Pleroma.Emoji.Formatter do
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
def emojify(text) do
|
||||
|
|
@ -44,7 +43,7 @@ defmodule Pleroma.Emoji.Formatter do
|
|||
Emoji.get_all()
|
||||
|> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end)
|
||||
|> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
|
||||
Map.put(acc, name, to_string(URI.merge(Endpoint.url(), file)))
|
||||
Map.put(acc, name, Emoji.local_url(file))
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -147,14 +147,22 @@ defmodule Pleroma.FollowingRelationship do
|
|||
|> Repo.aggregate(:count, :id)
|
||||
end
|
||||
|
||||
def get_follow_requests(%User{id: id}) do
|
||||
def get_follow_requests_query(%User{id: id}) do
|
||||
__MODULE__
|
||||
|> join(:inner, [r], f in assoc(r, :follower))
|
||||
|> join(:inner, [r], f in assoc(r, :follower), as: :follower)
|
||||
|> where([r], r.state == ^:follow_pending)
|
||||
|> where([r], r.following_id == ^id)
|
||||
|> where([r, f], f.is_active == true)
|
||||
|> select([r, f], f)
|
||||
|> Repo.all()
|
||||
|> where([r, follower: f], f.is_active == true)
|
||||
|> select([r, follower: f], f)
|
||||
end
|
||||
|
||||
def get_outgoing_follow_requests_query(%User{id: id}) do
|
||||
__MODULE__
|
||||
|> join(:inner, [r], f in assoc(r, :following), as: :following)
|
||||
|> where([r], r.state == ^:follow_pending)
|
||||
|> where([r], r.follower_id == ^id)
|
||||
|> where([r, following: f], f.is_active == true)
|
||||
|> select([r, following: f], f)
|
||||
end
|
||||
|
||||
def following?(%User{id: follower_id}, %User{id: followed_id}) do
|
||||
|
|
|
|||
|
|
@ -21,10 +21,13 @@ defmodule Pleroma.Gopher.Server do
|
|||
|
||||
def init([ip, port]) do
|
||||
Logger.info("Starting gopher server on #{port}")
|
||||
Process.flag(:trap_exit, true)
|
||||
|
||||
listener = :gopher
|
||||
|
||||
{:ok, _pid} =
|
||||
:ranch.start_listener(
|
||||
:gopher,
|
||||
listener,
|
||||
:ranch_tcp,
|
||||
%{
|
||||
num_acceptors: 100,
|
||||
|
|
@ -35,7 +38,11 @@ defmodule Pleroma.Gopher.Server do
|
|||
[]
|
||||
)
|
||||
|
||||
{:ok, %{ip: ip, port: port}}
|
||||
{:ok, %{ip: ip, port: port, listener: listener}}
|
||||
end
|
||||
|
||||
def terminate(_reason, state) do
|
||||
:ranch.stop_listener(state.listener)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -131,31 +131,4 @@ defmodule Pleroma.HTTP do
|
|||
|
||||
defp default_middleware,
|
||||
do: [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.EncodeUrl]
|
||||
|
||||
def encode_url(url) when is_binary(url) do
|
||||
URI.parse(url)
|
||||
|> then(fn parsed ->
|
||||
path = encode_path(parsed.path)
|
||||
query = encode_query(parsed.query)
|
||||
|
||||
%{parsed | path: path, query: query}
|
||||
end)
|
||||
|> URI.to_string()
|
||||
end
|
||||
|
||||
defp encode_path(nil), do: nil
|
||||
|
||||
defp encode_path(path) when is_binary(path) do
|
||||
path
|
||||
|> URI.decode()
|
||||
|> URI.encode()
|
||||
end
|
||||
|
||||
defp encode_query(nil), do: nil
|
||||
|
||||
defp encode_query(query) when is_binary(query) do
|
||||
query
|
||||
|> URI.decode_query()
|
||||
|> URI.encode_query()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
|
|||
@behaviour Pleroma.HTTP.AdapterHelper
|
||||
|
||||
@defaults [
|
||||
follow_redirect: true,
|
||||
force_redirect: true
|
||||
follow_redirect: false,
|
||||
force_redirect: false,
|
||||
with_body: true
|
||||
]
|
||||
|
||||
@spec options(keyword(), URI.t()) :: keyword()
|
||||
|
|
@ -16,7 +17,12 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
|
|||
|
||||
config_opts = Pleroma.Config.get([:http, :adapter], [])
|
||||
|
||||
url_encoding =
|
||||
Keyword.new()
|
||||
|> Keyword.put(:path_encode_fun, fn path -> path end)
|
||||
|
||||
@defaults
|
||||
|> Keyword.merge(url_encoding)
|
||||
|> Keyword.merge(config_opts)
|
||||
|> Keyword.merge(connection_opts)
|
||||
|> add_scheme_opts(uri)
|
||||
|
|
|
|||
109
lib/pleroma/language/translation/mozhi.ex
Normal file
109
lib/pleroma/language/translation/mozhi.ex
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.Mozhi do
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
alias Pleroma.Language.Translation.Provider
|
||||
|
||||
use Provider
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@name "Mozhi"
|
||||
|
||||
@impl Provider
|
||||
def configured?, do: not_empty_string(base_url()) and not_empty_string(engine())
|
||||
|
||||
@impl Provider
|
||||
def translate(content, source_language, target_language) do
|
||||
endpoint =
|
||||
base_url()
|
||||
|> URI.merge("/api/translate")
|
||||
|> URI.to_string()
|
||||
|
||||
case Pleroma.HTTP.get(
|
||||
endpoint <>
|
||||
"?" <>
|
||||
URI.encode_query(%{
|
||||
engine: engine(),
|
||||
text: content,
|
||||
from: source_language,
|
||||
to: target_language
|
||||
}),
|
||||
[{"Accept", "application/json"}]
|
||||
) do
|
||||
{:ok, %{status: 200} = res} ->
|
||||
%{
|
||||
"translated-text" => content,
|
||||
"source_language" => source_language
|
||||
} = Jason.decode!(res.body)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
content: content,
|
||||
detected_source_language: source_language,
|
||||
provider: @name
|
||||
}}
|
||||
|
||||
_ ->
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def supported_languages(type) when type in [:source, :target] do
|
||||
path =
|
||||
case type do
|
||||
:source -> "/api/source_languages"
|
||||
:target -> "/api/target_languages"
|
||||
end
|
||||
|
||||
endpoint =
|
||||
base_url()
|
||||
|> URI.merge(path)
|
||||
|> URI.to_string()
|
||||
|
||||
case Pleroma.HTTP.get(
|
||||
endpoint <>
|
||||
"?" <>
|
||||
URI.encode_query(%{
|
||||
engine: engine()
|
||||
}),
|
||||
[{"Accept", "application/json"}]
|
||||
) do
|
||||
{:ok, %{status: 200} = res} ->
|
||||
languages =
|
||||
Jason.decode!(res.body)
|
||||
|> Enum.map(fn %{"Id" => language} -> language end)
|
||||
|
||||
{:ok, languages}
|
||||
|
||||
_ ->
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def languages_matrix do
|
||||
with {:ok, source_languages} <- supported_languages(:source),
|
||||
{:ok, target_languages} <- supported_languages(:target) do
|
||||
{:ok,
|
||||
Map.new(source_languages, fn language -> {language, target_languages -- [language]} end)}
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def name, do: @name
|
||||
|
||||
defp base_url do
|
||||
Pleroma.Config.get([__MODULE__, :base_url])
|
||||
end
|
||||
|
||||
defp engine do
|
||||
Pleroma.Config.get([__MODULE__, :engine])
|
||||
end
|
||||
end
|
||||
129
lib/pleroma/language/translation/translate_locally.ex
Normal file
129
lib/pleroma/language/translation/translate_locally.ex
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.TranslateLocally do
|
||||
alias Pleroma.Language.Translation.Provider
|
||||
|
||||
use Provider
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@name "translateLocally"
|
||||
|
||||
@impl Provider
|
||||
def missing_dependencies do
|
||||
if Pleroma.Utils.command_available?("translateLocally") do
|
||||
[]
|
||||
else
|
||||
["translateLocally"]
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def configured?, do: is_map(models())
|
||||
|
||||
@impl Provider
|
||||
def translate(content, source_language, target_language) do
|
||||
model =
|
||||
models()
|
||||
|> Map.get(source_language, %{})
|
||||
|> Map.get(target_language)
|
||||
|
||||
models =
|
||||
if model do
|
||||
[model]
|
||||
else
|
||||
[
|
||||
models()
|
||||
|> Map.get(source_language, %{})
|
||||
|> Map.get(intermediary_language()),
|
||||
models()
|
||||
|> Map.get(intermediary_language(), %{})
|
||||
|> Map.get(target_language)
|
||||
]
|
||||
end
|
||||
|
||||
translated_content =
|
||||
Enum.reduce(models, content, fn model, content ->
|
||||
text_path = Path.join(System.tmp_dir!(), "translateLocally-#{Ecto.UUID.generate()}")
|
||||
|
||||
File.write(text_path, content)
|
||||
|
||||
translated_content =
|
||||
case System.cmd("translateLocally", ["-m", model, "-i", text_path, "--html"]) do
|
||||
{content, _} -> content
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
File.rm(text_path)
|
||||
|
||||
translated_content
|
||||
end)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
content: translated_content,
|
||||
detected_source_language: source_language,
|
||||
provider: @name
|
||||
}}
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def supported_languages(:source) do
|
||||
languages =
|
||||
languages_matrix()
|
||||
|> elem(1)
|
||||
|> Map.keys()
|
||||
|
||||
{:ok, languages}
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def supported_languages(:target) do
|
||||
languages =
|
||||
languages_matrix()
|
||||
|> elem(1)
|
||||
|> Map.values()
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, languages}
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def languages_matrix do
|
||||
languages =
|
||||
models()
|
||||
|> Map.to_list()
|
||||
|> Enum.map(fn {key, value} -> {key, Map.keys(value)} end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
matrix =
|
||||
if intermediary_language() do
|
||||
languages
|
||||
|> Map.to_list()
|
||||
|> Enum.map(fn {key, value} ->
|
||||
with_intermediary =
|
||||
(((value ++ languages[intermediary_language()])
|
||||
|> Enum.uniq()) --
|
||||
[key])
|
||||
|> Enum.sort()
|
||||
|
||||
{key, with_intermediary}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
else
|
||||
languages
|
||||
end
|
||||
|
||||
{:ok, matrix}
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def name, do: @name
|
||||
|
||||
defp models, do: Pleroma.Config.get([__MODULE__, :models])
|
||||
|
||||
defp intermediary_language, do: Pleroma.Config.get([__MODULE__, :intermediary_language])
|
||||
end
|
||||
|
|
@ -17,13 +17,14 @@ defmodule Pleroma.List do
|
|||
field(:title, :string)
|
||||
field(:following, {:array, :string}, default: [])
|
||||
field(:ap_id, :string)
|
||||
field(:exclusive, :boolean, default: false)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def title_changeset(list, attrs \\ %{}) do
|
||||
def update_changeset(list, attrs \\ %{}) do
|
||||
list
|
||||
|> cast(attrs, [:title])
|
||||
|> cast(attrs, [:title, :exclusive])
|
||||
|> validate_required([:title])
|
||||
end
|
||||
|
||||
|
|
@ -91,14 +92,14 @@ defmodule Pleroma.List do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
def rename(%Pleroma.List{} = list, title) do
|
||||
def update(%Pleroma.List{} = list, params) do
|
||||
list
|
||||
|> title_changeset(%{title: title})
|
||||
|> update_changeset(params)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def create(title, %User{} = creator) do
|
||||
changeset = title_changeset(%Pleroma.List{user_id: creator.id}, %{title: title})
|
||||
def create(params, %User{} = creator) do
|
||||
changeset = update_changeset(%Pleroma.List{user_id: creator.id}, params)
|
||||
|
||||
if changeset.valid? do
|
||||
Repo.transaction(fn ->
|
||||
|
|
@ -149,4 +150,14 @@ defmodule Pleroma.List do
|
|||
end
|
||||
|
||||
def member?(_, _), do: false
|
||||
|
||||
def get_exclusive_list_members(%User{id: user_id}) do
|
||||
Pleroma.List
|
||||
|> where([l], l.user_id == ^user_id)
|
||||
|> where([l], l.exclusive == true)
|
||||
|> select([l], l.following)
|
||||
|> Repo.all()
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ defmodule Pleroma.Marker do
|
|||
|
||||
defp get_marker(user, timeline) do
|
||||
case Repo.find_resource(get_query(user, timeline)) do
|
||||
{:ok, marker} -> %__MODULE__{marker | user: user}
|
||||
{:ok, %__MODULE__{} = marker} -> %{marker | user: user}
|
||||
_ -> %__MODULE__{timeline: timeline, user_id: user.id}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ defmodule Pleroma.MFA.Changeset do
|
|||
alias Pleroma.User
|
||||
|
||||
def disable(%Ecto.Changeset{} = changeset, force \\ false) do
|
||||
settings =
|
||||
%Settings{} =
|
||||
settings =
|
||||
changeset
|
||||
|> Ecto.Changeset.apply_changes()
|
||||
|> MFA.fetch_settings()
|
||||
|
|
@ -20,20 +21,20 @@ defmodule Pleroma.MFA.Changeset do
|
|||
end
|
||||
end
|
||||
|
||||
def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do
|
||||
def disable_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do
|
||||
user
|
||||
|> put_change(%Settings{settings | totp: %Settings.TOTP{}})
|
||||
end
|
||||
|
||||
def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do
|
||||
totp_settings = %Settings.TOTP{settings.totp | confirmed: true}
|
||||
def confirm_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do
|
||||
totp_settings = %Settings.TOTP{(%Settings.TOTP{} = settings.totp) | confirmed: true}
|
||||
|
||||
user
|
||||
|> put_change(%Settings{settings | totp: totp_settings, enabled: true})
|
||||
end
|
||||
|
||||
def setup_totp(%User{} = user, attrs) do
|
||||
mfa_settings = MFA.fetch_settings(user)
|
||||
%Settings{} = mfa_settings = MFA.fetch_settings(user)
|
||||
|
||||
totp_settings =
|
||||
%Settings.TOTP{}
|
||||
|
|
@ -46,7 +47,7 @@ defmodule Pleroma.MFA.Changeset do
|
|||
def cast_backup_codes(%User{} = user, codes) do
|
||||
user
|
||||
|> put_change(%Settings{
|
||||
user.multi_factor_authentication_settings
|
||||
(%Settings{} = user.multi_factor_authentication_settings)
|
||||
| backup_codes: codes
|
||||
})
|
||||
end
|
||||
|
|
|
|||
|
|
@ -132,11 +132,18 @@ defmodule Pleroma.ModerationLog do
|
|||
end
|
||||
|
||||
def insert_log(%{actor: %User{}, action: action, subject: %Activity{} = subject} = attrs)
|
||||
when action in ["report_note_delete", "report_update", "report_note"] do
|
||||
when action in [
|
||||
"report_note_delete",
|
||||
"report_update",
|
||||
"report_note",
|
||||
"report_unassigned",
|
||||
"report_assigned"
|
||||
] do
|
||||
data =
|
||||
attrs
|
||||
|> prepare_log_data
|
||||
|> Pleroma.Maps.put_if_present("text", attrs[:text])
|
||||
|> Pleroma.Maps.put_if_present("assigned_account", attrs[:assigned_account])
|
||||
|> Map.merge(%{"subject" => report_to_map(subject)})
|
||||
|
||||
insert_log_entry_with_message(%ModerationLog{data: data})
|
||||
|
|
@ -441,6 +448,35 @@ defmodule Pleroma.ModerationLog do
|
|||
" with '#{state}' state"
|
||||
end
|
||||
|
||||
def get_log_entry_message(
|
||||
%ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "report_assigned",
|
||||
"subject" => %{"id" => subject_id, "type" => "report"},
|
||||
"assigned_account" => assigned_account
|
||||
}
|
||||
} = log
|
||||
) do
|
||||
"@#{actor_nickname} assigned report ##{subject_id}" <>
|
||||
subject_actor_nickname(log, " (on user ", ")") <>
|
||||
" to user #{assigned_account}"
|
||||
end
|
||||
|
||||
def get_log_entry_message(
|
||||
%ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "report_unassigned",
|
||||
"subject" => %{"id" => subject_id, "type" => "report"}
|
||||
}
|
||||
} = log
|
||||
) do
|
||||
"@#{actor_nickname} unassigned report ##{subject_id}" <>
|
||||
subject_actor_nickname(log, " (on user ", ")") <>
|
||||
" from a user"
|
||||
end
|
||||
|
||||
def get_log_entry_message(
|
||||
%ModerationLog{
|
||||
data: %{
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ defmodule Pleroma.Notification do
|
|||
reblog
|
||||
poll
|
||||
status
|
||||
update
|
||||
}
|
||||
|
||||
def changeset(%Notification{} = notification, attrs) do
|
||||
|
|
@ -281,10 +282,15 @@ defmodule Pleroma.Notification do
|
|||
select: n.id
|
||||
)
|
||||
|
||||
Multi.new()
|
||||
|> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
|
||||
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||
|> Repo.transaction()
|
||||
{:ok, %{marker: marker}} =
|
||||
Multi.new()
|
||||
|> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
|
||||
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||
|> Repo.transaction()
|
||||
|
||||
Streamer.stream(["user", "user:notification"], marker)
|
||||
|
||||
{:ok, %{marker: marker}}
|
||||
end
|
||||
|
||||
@spec read_one(User.t(), String.t()) ::
|
||||
|
|
@ -525,9 +531,7 @@ defmodule Pleroma.Notification do
|
|||
%Activity{data: %{"type" => "Create"}} = activity,
|
||||
local_only
|
||||
) do
|
||||
notification_enabled_ap_ids =
|
||||
[]
|
||||
|> Utils.maybe_notify_subscribers(activity)
|
||||
notification_enabled_ap_ids = Utils.get_notified_subscribers(activity)
|
||||
|
||||
potential_receivers =
|
||||
User.get_users_from_set(notification_enabled_ap_ids, local_only: local_only)
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ defmodule Pleroma.Object do
|
|||
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
|
||||
end
|
||||
|
||||
def normalize(_, options \\ [fetch: false, id_only: false])
|
||||
def normalize(_, options \\ [fetch: false])
|
||||
|
||||
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
|
||||
# Use this whenever possible, especially when walking graphs in an O(N) loop!
|
||||
|
|
@ -155,9 +155,6 @@ defmodule Pleroma.Object do
|
|||
|
||||
def normalize(ap_id, options) when is_binary(ap_id) do
|
||||
cond do
|
||||
Keyword.get(options, :id_only) ->
|
||||
ap_id
|
||||
|
||||
Keyword.get(options, :fetch) ->
|
||||
case Fetcher.fetch_object_from_id(ap_id, options) do
|
||||
{:ok, object} -> object
|
||||
|
|
@ -377,10 +374,18 @@ defmodule Pleroma.Object do
|
|||
|
||||
voters = [actor | object.data["voters"] || []] |> Enum.uniq()
|
||||
|
||||
voters_count =
|
||||
if Map.has_key?(object.data, "votersCount") do
|
||||
object.data["votersCount"] + 1
|
||||
else
|
||||
length(voters)
|
||||
end
|
||||
|
||||
data =
|
||||
object.data
|
||||
|> Map.put(key, options)
|
||||
|> Map.put("voters", voters)
|
||||
|> Map.put("votersCount", voters_count)
|
||||
|
||||
object
|
||||
|> Object.change(%{data: data})
|
||||
|
|
@ -401,28 +406,6 @@ defmodule Pleroma.Object do
|
|||
String.starts_with?(id, Pleroma.Web.Endpoint.url() <> "/")
|
||||
end
|
||||
|
||||
def replies(object, opts \\ []) do
|
||||
object = Object.normalize(object, fetch: false)
|
||||
|
||||
query =
|
||||
Object
|
||||
|> where(
|
||||
[o],
|
||||
fragment("(?)->>'inReplyTo' = ?", o.data, ^object.data["id"])
|
||||
)
|
||||
|> order_by([o], asc: o.id)
|
||||
|
||||
if opts[:self_only] do
|
||||
actor = object.data["actor"]
|
||||
where(query, [o], fragment("(?)->>'actor' = ?", o.data, ^actor))
|
||||
else
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
def self_replies(object, opts \\ []),
|
||||
do: replies(object, Keyword.put(opts, :self_only, true))
|
||||
|
||||
def tags(%Object{data: %{"tag" => tags}}) when is_list(tags), do: tags
|
||||
|
||||
def tags(_), do: []
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Object.Updater do
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
||||
|
|
@ -115,6 +116,7 @@ defmodule Pleroma.Object.Updater do
|
|||
# Choices are the same, but counts are different
|
||||
to_be_updated
|
||||
|> Map.put(key, updated_object[key])
|
||||
|> Maps.put_if_present("votersCount", updated_object["votersCount"])
|
||||
else
|
||||
# Choices (or vote type) have changed, do not allow this
|
||||
_ -> to_be_updated
|
||||
|
|
|
|||
|
|
@ -95,13 +95,30 @@ defmodule Pleroma.Pagination do
|
|||
offset: :integer,
|
||||
limit: :integer,
|
||||
skip_extra_order: :boolean,
|
||||
skip_order: :boolean
|
||||
skip_order: :boolean,
|
||||
order_asc: :boolean
|
||||
}
|
||||
|
||||
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
|
||||
changeset.changes
|
||||
end
|
||||
|
||||
defp order_statement(query, table_binding, :asc) do
|
||||
order_by(
|
||||
query,
|
||||
[{u, table_position(query, table_binding)}],
|
||||
fragment("? asc nulls last", u.id)
|
||||
)
|
||||
end
|
||||
|
||||
defp order_statement(query, table_binding, :desc) do
|
||||
order_by(
|
||||
query,
|
||||
[{u, table_position(query, table_binding)}],
|
||||
fragment("? desc nulls last", u.id)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict(query, :min_id, %{min_id: min_id}, table_binding) do
|
||||
where(query, [{q, table_position(query, table_binding)}], q.id > ^min_id)
|
||||
end
|
||||
|
|
@ -119,19 +136,16 @@ defmodule Pleroma.Pagination do
|
|||
defp restrict(%{order_bys: [_ | _]} = query, :order, %{skip_extra_order: true}, _), do: query
|
||||
|
||||
defp restrict(query, :order, %{min_id: _}, table_binding) do
|
||||
order_by(
|
||||
query,
|
||||
[{u, table_position(query, table_binding)}],
|
||||
fragment("? asc nulls last", u.id)
|
||||
)
|
||||
order_statement(query, table_binding, :asc)
|
||||
end
|
||||
|
||||
defp restrict(query, :order, _options, table_binding) do
|
||||
order_by(
|
||||
query,
|
||||
[{u, table_position(query, table_binding)}],
|
||||
fragment("? desc nulls last", u.id)
|
||||
)
|
||||
defp restrict(query, :order, %{max_id: _}, table_binding) do
|
||||
order_statement(query, table_binding, :desc)
|
||||
end
|
||||
|
||||
defp restrict(query, :order, options, table_binding) do
|
||||
dir = if options[:order_asc], do: :asc, else: :desc
|
||||
order_statement(query, table_binding, dir)
|
||||
end
|
||||
|
||||
defp restrict(query, :offset, %{offset: offset}, _table_binding) do
|
||||
|
|
@ -151,11 +165,9 @@ defmodule Pleroma.Pagination do
|
|||
|
||||
defp restrict(query, _, _, _), do: query
|
||||
|
||||
defp enforce_order(result, %{min_id: _}) do
|
||||
result
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
defp enforce_order(result, %{min_id: _, order_asc: true}), do: result
|
||||
defp enforce_order(result, %{min_id: _}), do: Enum.reverse(result)
|
||||
defp enforce_order(result, %{max_id: _, order_asc: true}), do: Enum.reverse(result)
|
||||
defp enforce_order(result, _), do: result
|
||||
|
||||
defp table_position(%Ecto.Query{} = query, binding_name) do
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
defmodule Pleroma.ReleaseTasks do
|
||||
@repo Pleroma.Repo
|
||||
|
||||
def run(args) do
|
||||
# TODO: Kept for some backwards compatibility with buggy pleroma_ctl,
|
||||
# if a mismatch between pleroma_ctl and Pleroma accidentaly happens.
|
||||
# Remove in the future.
|
||||
def run(args) when is_binary(args) do
|
||||
[task | args] = String.split(args)
|
||||
|
||||
case task do
|
||||
|
|
@ -16,6 +19,20 @@ defmodule Pleroma.ReleaseTasks do
|
|||
end
|
||||
end
|
||||
|
||||
# HACK: Script arguments need to be received as a list, otherwise (quoted) arguments with
|
||||
# whitespace will be broken. Previously the broken string form above was used,
|
||||
# escaping in the shell does not help.
|
||||
def run(args) when is_list(args) do
|
||||
[task | args] = args
|
||||
|
||||
case task do
|
||||
"migrate" -> migrate(args)
|
||||
"create" -> create()
|
||||
"rollback" -> rollback(args)
|
||||
task -> mix_task(task, args)
|
||||
end
|
||||
end
|
||||
|
||||
def find_module(task) do
|
||||
module_name =
|
||||
task
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ReverseProxy do
|
||||
alias Pleroma.Utils.URIEncoding
|
||||
|
||||
@range_headers ~w(range if-range)
|
||||
@keep_req_headers ~w(accept accept-encoding cache-control if-modified-since) ++
|
||||
~w(if-unmodified-since if-none-match) ++ @range_headers
|
||||
|
|
@ -10,7 +12,7 @@ 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"
|
||||
@default_cache_control_header "public, max-age=1209600, immutable"
|
||||
@valid_resp_codes [200, 206, 304]
|
||||
@max_read_duration :timer.seconds(30)
|
||||
@max_body_length :infinity
|
||||
|
|
@ -155,11 +157,12 @@ defmodule Pleroma.ReverseProxy do
|
|||
end
|
||||
|
||||
defp request(method, url, headers, opts) do
|
||||
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
|
||||
method = method |> String.downcase() |> String.to_existing_atom()
|
||||
|
||||
url = maybe_encode_url(url)
|
||||
|
||||
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
|
||||
|
||||
case client().request(method, url, headers, "", opts) do
|
||||
{:ok, code, headers, client} when code in @valid_resp_codes ->
|
||||
{:ok, code, downcase_headers(headers), client}
|
||||
|
|
@ -459,9 +462,9 @@ defmodule Pleroma.ReverseProxy do
|
|||
# Also do it for test environment
|
||||
defp maybe_encode_url(url) do
|
||||
case Application.get_env(:tesla, :adapter) do
|
||||
Tesla.Adapter.Hackney -> Pleroma.HTTP.encode_url(url)
|
||||
{Tesla.Adapter.Finch, _} -> Pleroma.HTTP.encode_url(url)
|
||||
Tesla.Mock -> Pleroma.HTTP.encode_url(url)
|
||||
Tesla.Adapter.Hackney -> URIEncoding.encode_url(url)
|
||||
{Tesla.Adapter.Finch, _} -> URIEncoding.encode_url(url)
|
||||
Tesla.Mock -> URIEncoding.encode_url(url)
|
||||
_ -> url
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,10 +4,52 @@
|
|||
|
||||
defmodule Pleroma.ReverseProxy.Client.Hackney do
|
||||
@behaviour Pleroma.ReverseProxy.Client
|
||||
@redirect_limit 5
|
||||
|
||||
require Logger
|
||||
|
||||
# In-app redirect handler to avoid Hackney redirect bugs:
|
||||
# - https://github.com/benoitc/hackney/issues/527 (relative/protocol-less redirects can crash Hackney)
|
||||
# - https://github.com/benoitc/hackney/issues/273 (redirects not followed when using HTTP proxy)
|
||||
#
|
||||
# Based on a redirect handler from Pleb, slightly modified to work with Hackney:
|
||||
# https://declin.eu/objects/d4f38e62-5429-4614-86d1-e8fc16e6bf33
|
||||
@redirect_statuses [301, 302, 303, 307, 308]
|
||||
defp absolute_redirect_url(original_url, resp_headers) do
|
||||
location =
|
||||
Enum.find(resp_headers, fn {header, _location} ->
|
||||
String.downcase(header) == "location"
|
||||
end)
|
||||
|
||||
URI.merge(original_url, elem(location, 1))
|
||||
|> URI.to_string()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def request(method, url, headers, body, opts \\ []) do
|
||||
:hackney.request(method, url, headers, body, opts)
|
||||
opts =
|
||||
Keyword.put_new(opts, :path_encode_fun, fn path ->
|
||||
path
|
||||
end)
|
||||
|
||||
if opts[:follow_redirect] != false do
|
||||
{_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end)
|
||||
env = %{method: method, headers: headers, body: body, req_opts: req_opts}
|
||||
res = :hackney.request(method, url, headers, body, req_opts)
|
||||
|
||||
case res do
|
||||
{:ok, code, resp_headers, _client} when code in @redirect_statuses ->
|
||||
redirect(url, resp_headers, env, @redirect_limit)
|
||||
|
||||
{:ok, code, resp_headers} when code in @redirect_statuses ->
|
||||
redirect(url, resp_headers, env, @redirect_limit)
|
||||
|
||||
_ ->
|
||||
res
|
||||
end
|
||||
else
|
||||
:hackney.request(method, url, headers, body, opts)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -21,4 +63,32 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
|
|||
|
||||
@impl true
|
||||
def close(ref), do: :hackney.close(ref)
|
||||
|
||||
defp redirect(url, resp_headers, env, limit) when limit == 0 do
|
||||
new_url = absolute_redirect_url(url, resp_headers)
|
||||
|
||||
Logger.debug(
|
||||
"#{__MODULE__}: Handling redirect #{url} -> #{new_url}; redirect limit was reached - returning response after final redirect"
|
||||
)
|
||||
|
||||
:hackney.request(env.method, new_url, env.headers, env.body, env.req_opts)
|
||||
end
|
||||
|
||||
defp redirect(url, resp_headers, env, limit) do
|
||||
new_url = absolute_redirect_url(url, resp_headers)
|
||||
Logger.debug("#{__MODULE__}: handling redirect #{url} -> #{new_url}; limit = #{limit}")
|
||||
|
||||
res = :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts)
|
||||
|
||||
case res do
|
||||
{:ok, code, new_resp_headers, _client} when code in @redirect_statuses ->
|
||||
redirect(new_url, new_resp_headers, env, limit - 1)
|
||||
|
||||
{:ok, code, new_resp_headers} when code in @redirect_statuses ->
|
||||
redirect(new_url, new_resp_headers, env, limit - 1)
|
||||
|
||||
_ ->
|
||||
res
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,11 +1,28 @@
|
|||
defmodule Pleroma.Search do
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Workers.SearchIndexingWorker
|
||||
|
||||
def add_to_index(%Pleroma.Activity{id: activity_id}) do
|
||||
SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => activity_id})
|
||||
|> Oban.insert()
|
||||
@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
|
||||
SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => activity_id})
|
||||
|> Oban.insert()
|
||||
else
|
||||
_ -> {:ok, :noop}
|
||||
end
|
||||
end
|
||||
|
||||
def add_to_index(%Activity{id: activity_id}) do
|
||||
case Activity.get_by_id_with_object(activity_id) do
|
||||
%Activity{} = preloaded -> add_to_index(preloaded)
|
||||
_ -> {:ok, :noop}
|
||||
end
|
||||
end
|
||||
|
||||
@spec remove_from_index(Object.t()) :: {:ok, Oban.Job.t()} | {:error, Oban.Job.changeset()}
|
||||
def remove_from_index(%Pleroma.Object{id: object_id}) do
|
||||
SearchIndexingWorker.new(%{"op" => "remove_from_index", "object" => object_id})
|
||||
|> Oban.insert()
|
||||
|
|
@ -20,4 +37,44 @@ defmodule Pleroma.Search do
|
|||
search_module = Pleroma.Config.get([Pleroma.Search, :module])
|
||||
search_module.healthcheck_endpoints()
|
||||
end
|
||||
|
||||
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"])
|
||||
|
||||
%{
|
||||
id: object.id,
|
||||
content: content,
|
||||
ap: data["id"],
|
||||
published: published |> DateTime.to_unix()
|
||||
}
|
||||
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
|
||||
|
||||
defp indexable?(_), do: false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ defmodule Pleroma.Search.Meilisearch do
|
|||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config.Getting, as: Config
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Search
|
||||
|
||||
import Pleroma.Search.DatabaseSearch
|
||||
import Ecto.Query
|
||||
|
|
@ -118,66 +120,24 @@ defmodule Pleroma.Search.Meilisearch do
|
|||
end
|
||||
end
|
||||
|
||||
def object_to_search_data(object) do
|
||||
# Only index public or unlisted Notes
|
||||
if not is_nil(object) and object.data["type"] == "Note" and
|
||||
not is_nil(object.data["content"]) and
|
||||
not is_nil(object.data["published"]) and
|
||||
(Pleroma.Constants.as_public() in object.data["to"] or
|
||||
Pleroma.Constants.as_public() in object.data["cc"]) and
|
||||
object.data["content"] not in ["", "."] 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"])
|
||||
|
||||
%{
|
||||
id: object.id,
|
||||
content: content,
|
||||
ap: data["id"],
|
||||
published: published |> DateTime.to_unix()
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def add_to_index(activity) do
|
||||
maybe_search_data = object_to_search_data(activity.object)
|
||||
def add_to_index(%Activity{object: %Object{} = object} = activity) do
|
||||
search_data = Search.object_to_search_data(object)
|
||||
|
||||
if activity.data["type"] == "Create" and maybe_search_data do
|
||||
result =
|
||||
meili_put(
|
||||
"/indexes/objects/documents",
|
||||
[maybe_search_data]
|
||||
)
|
||||
result =
|
||||
meili_put(
|
||||
"/indexes/objects/documents",
|
||||
[search_data]
|
||||
)
|
||||
|
||||
with {:ok, %{"status" => "enqueued"}} <- result do
|
||||
# Added successfully
|
||||
:ok
|
||||
else
|
||||
_ ->
|
||||
# There was an error, report it
|
||||
Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
|
||||
{:error, result}
|
||||
end
|
||||
else
|
||||
# The post isn't something we can search, that's ok
|
||||
with {:ok, %{"status" => "enqueued"}} <- result do
|
||||
# Added successfully
|
||||
:ok
|
||||
else
|
||||
_ ->
|
||||
# There was an error, report it
|
||||
Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
|
||||
{:error, result}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ defmodule Pleroma.Search.QdrantSearch do
|
|||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config.Getting, as: Config
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Search
|
||||
|
||||
alias __MODULE__.OpenAIClient
|
||||
alias __MODULE__.QdrantClient
|
||||
|
||||
import Pleroma.Search.Meilisearch, only: [object_to_search_data: 1]
|
||||
import Pleroma.Search.DatabaseSearch, only: [maybe_fetch: 3]
|
||||
|
||||
@impl true
|
||||
|
|
@ -82,23 +83,18 @@ defmodule Pleroma.Search.QdrantSearch do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def add_to_index(activity) do
|
||||
# This will only index public or unlisted notes
|
||||
maybe_search_data = object_to_search_data(activity.object)
|
||||
def add_to_index(%Activity{object: %Object{} = object} = activity) do
|
||||
search_data = Search.object_to_search_data(object)
|
||||
|
||||
if activity.data["type"] == "Create" and maybe_search_data do
|
||||
with {:ok, embedding} <- get_embedding(maybe_search_data.content),
|
||||
{:ok, %{status: 200}} <-
|
||||
QdrantClient.put(
|
||||
"/collections/posts/points",
|
||||
build_index_payload(activity, embedding)
|
||||
) do
|
||||
:ok
|
||||
else
|
||||
e -> {:error, e}
|
||||
end
|
||||
else
|
||||
with {:ok, embedding} <- get_embedding(search_data.content),
|
||||
{:ok, %{status: 200}} <-
|
||||
QdrantClient.put(
|
||||
"/collections/posts/points",
|
||||
build_index_payload(activity, embedding)
|
||||
) do
|
||||
:ok
|
||||
else
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ defmodule Pleroma.Signature do
|
|||
|
||||
def fetch_public_key(conn) do
|
||||
with {:ok, actor_id} <- get_actor_id(conn),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||
{:ok, public_key} <- User.get_or_fetch_public_key_for_ap_id(actor_id) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
e ->
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ defmodule Pleroma.Tesla.Middleware.EncodeUrl do
|
|||
|
||||
@impl Tesla.Middleware
|
||||
def call(%Tesla.Env{url: url} = env, next, _) do
|
||||
url = Pleroma.HTTP.encode_url(url)
|
||||
url = Pleroma.Utils.URIEncoding.encode_url(url)
|
||||
|
||||
env = %{env | url: url}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ defmodule Pleroma.Upload do
|
|||
"""
|
||||
alias Ecto.UUID
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Utils.URIEncoding
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
require Logger
|
||||
|
||||
|
|
@ -92,7 +93,7 @@ defmodule Pleroma.Upload do
|
|||
def store(upload, opts \\ []) do
|
||||
opts = get_opts(opts)
|
||||
|
||||
with {:ok, upload} <- prepare_upload(upload, opts),
|
||||
with {:ok, %__MODULE__{} = upload} <- prepare_upload(upload, opts),
|
||||
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
|
||||
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
|
||||
description = get_description(upload),
|
||||
|
|
@ -230,11 +231,18 @@ defmodule Pleroma.Upload do
|
|||
tmp_path
|
||||
end
|
||||
|
||||
# Encoding the whole path here is fine since the path is in a
|
||||
# UUID/<file name> form.
|
||||
# The file at this point isn't %-encoded, so the path shouldn't
|
||||
# be decoded first like Pleroma.Utils.URIEncoding.encode_url/1 does.
|
||||
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
|
||||
encode_opts = [bypass_decode: true, bypass_parse: true]
|
||||
|
||||
path =
|
||||
URI.encode(path, &char_unescaped?/1) <>
|
||||
URIEncoding.encode_url(path, encode_opts) <>
|
||||
if Pleroma.Config.get([__MODULE__, :link_name], false) do
|
||||
"?name=#{URI.encode(name, &char_unescaped?/1)}"
|
||||
enum = %{name: name}
|
||||
"?#{URI.encode_query(enum)}"
|
||||
else
|
||||
""
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,22 +29,26 @@ defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do
|
|||
do: current_description
|
||||
|
||||
defp read_when_empty(_, file, tag) do
|
||||
try do
|
||||
{tag_content, 0} =
|
||||
System.cmd("exiftool", ["-b", "-s3", tag, file],
|
||||
stderr_to_stdout: false,
|
||||
parallelism: true
|
||||
)
|
||||
if File.exists?(file) do
|
||||
try do
|
||||
{tag_content, 0} =
|
||||
System.cmd("exiftool", ["-m", "-b", "-s3", tag, file],
|
||||
stderr_to_stdout: false,
|
||||
parallelism: true
|
||||
)
|
||||
|
||||
tag_content = String.trim(tag_content)
|
||||
tag_content = String.trim(tag_content)
|
||||
|
||||
if tag_content != "" and
|
||||
String.length(tag_content) <=
|
||||
Pleroma.Config.get([:instance, :description_limit]),
|
||||
do: tag_content,
|
||||
else: nil
|
||||
rescue
|
||||
_ in ErlangError -> nil
|
||||
if tag_content != "" and
|
||||
String.length(tag_content) <=
|
||||
Pleroma.Config.get([:instance, :description_limit]),
|
||||
do: tag_content,
|
||||
else: nil
|
||||
rescue
|
||||
_ in ErlangError -> nil
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do
|
|||
|
||||
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
|
||||
try do
|
||||
case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", "-png:all=", file],
|
||||
case System.cmd("exiftool", ["-m", "-overwrite_original", "-gps:all=", "-png:all=", file],
|
||||
stderr_to_stdout: true,
|
||||
parallelism: true
|
||||
) do
|
||||
{_response, 0} -> {:ok, :filtered}
|
||||
{error, 1} -> {:error, error}
|
||||
{error, _} -> {:error, error}
|
||||
end
|
||||
rescue
|
||||
e in ErlangError ->
|
||||
|
|
|
|||
|
|
@ -234,8 +234,8 @@ defmodule Pleroma.User do
|
|||
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
|
||||
@user_relationships_config do
|
||||
# `def blocked_users_relation/2`, `def muted_users_relation/2`,
|
||||
# `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
|
||||
# `def subscriber_users/2`, `def endorsed_users_relation/2`
|
||||
# `def reblog_muted_users_relation/2`, `def notification_muted_users_relation/2`,
|
||||
# `def subscriber_users_relation/2`, `def endorsed_users_relation/2`
|
||||
def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
|
||||
target_users_query = assoc(user, unquote(outgoing_relation_target))
|
||||
|
||||
|
|
@ -288,7 +288,14 @@ defmodule Pleroma.User do
|
|||
defdelegate following(user), to: FollowingRelationship
|
||||
defdelegate following?(follower, followed), to: FollowingRelationship
|
||||
defdelegate following_ap_ids(user), to: FollowingRelationship
|
||||
defdelegate get_follow_requests(user), to: FollowingRelationship
|
||||
defdelegate get_follow_requests_query(user), to: FollowingRelationship
|
||||
defdelegate get_outgoing_follow_requests_query(user), to: FollowingRelationship
|
||||
|
||||
def get_follow_requests(user) do
|
||||
get_follow_requests_query(user)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
defdelegate search(query, opts \\ []), to: User.Search
|
||||
|
||||
@doc """
|
||||
|
|
@ -1359,7 +1366,7 @@ defmodule Pleroma.User do
|
|||
@spec get_by_nickname(String.t()) :: User.t() | nil
|
||||
def get_by_nickname(nickname) do
|
||||
Repo.get_by(User, nickname: nickname) ||
|
||||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
|
||||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()}$)i, nickname) do
|
||||
Repo.get_by(User, nickname: local_nickname(nickname))
|
||||
end
|
||||
end
|
||||
|
|
@ -2309,6 +2316,15 @@ defmodule Pleroma.User do
|
|||
|
||||
def public_key(_), do: {:error, "key not found"}
|
||||
|
||||
def get_or_fetch_public_key_for_ap_id(ap_id) do
|
||||
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
|
||||
{:ok, public_key} <- public_key(user) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def get_public_key_for_ap_id(ap_id) do
|
||||
with %User{} = user <- get_cached_by_ap_id(ap_id),
|
||||
{:ok, public_key} <- public_key(user) do
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ defmodule Pleroma.User.Backup do
|
|||
dir,
|
||||
"outbox",
|
||||
fn a ->
|
||||
with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
|
||||
with {:ok, activity} <- Transmogrifier.prepare_activity(a.data) do
|
||||
{:ok, Map.delete(activity, "@context")}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ defmodule Pleroma.User.Search do
|
|||
following = Keyword.get(opts, :following, false)
|
||||
result_limit = Keyword.get(opts, :limit, @limit)
|
||||
offset = Keyword.get(opts, :offset, 0)
|
||||
capabilities = Keyword.get(opts, :capabilities, [])
|
||||
|
||||
for_user = Keyword.get(opts, :for_user)
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ defmodule Pleroma.User.Search do
|
|||
|
||||
results =
|
||||
query_string
|
||||
|> search_query(for_user, following, top_user_ids)
|
||||
|> search_query(for_user, following, top_user_ids, capabilities)
|
||||
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
|
||||
|
||||
results
|
||||
|
|
@ -80,7 +81,7 @@ defmodule Pleroma.User.Search do
|
|||
end
|
||||
end
|
||||
|
||||
defp search_query(query_string, for_user, following, top_user_ids) do
|
||||
defp search_query(query_string, for_user, following, top_user_ids, capabilities) do
|
||||
for_user
|
||||
|> base_query(following)
|
||||
|> filter_blocked_user(for_user)
|
||||
|
|
@ -94,6 +95,7 @@ defmodule Pleroma.User.Search do
|
|||
|> subquery()
|
||||
|> order_by(desc: :search_rank)
|
||||
|> maybe_restrict_local(for_user)
|
||||
|> maybe_restrict_accepting_chat_messages(capabilities)
|
||||
|> filter_deactivated_users()
|
||||
end
|
||||
|
||||
|
|
@ -214,6 +216,14 @@ defmodule Pleroma.User.Search do
|
|||
end
|
||||
end
|
||||
|
||||
defp maybe_restrict_accepting_chat_messages(query, capabilities) do
|
||||
if "accepts_chat_messages" in capabilities do
|
||||
from(q in query, where: q.accepts_chat_messages == true)
|
||||
else
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
|
||||
|
||||
defp restrict_local(q), do: where(q, [u], u.local == true)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ defmodule Pleroma.UserRelationship do
|
|||
do: exists?(unquote(relationship_type), source, target)
|
||||
|
||||
# `def get_block_expire_date/2`, `def get_mute_expire_date/2`,
|
||||
# `def get_reblog_mute_expire_date/2`, `def get_notification_mute_exists?/2`,
|
||||
# `def get_reblog_mute_expire_date/2`, `def get_notification_mute_expire_date/2`,
|
||||
# `def get_inverse_subscription_expire_date/2`, `def get_inverse_endorsement_expire_date/2`
|
||||
def unquote(:"get_#{relationship_type}_expire_date")(source, target),
|
||||
do: get_expire_date(unquote(relationship_type), source, target)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ defmodule Pleroma.Utils do
|
|||
dir
|
||||
|> File.ls!()
|
||||
|> Enum.map(&Path.join(dir, &1))
|
||||
|> Kernel.ParallelCompiler.compile()
|
||||
|> Kernel.ParallelCompiler.compile(return_diagnostics: true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
142
lib/pleroma/utils/uri_encoding.ex
Normal file
142
lib/pleroma/utils/uri_encoding.ex
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2025 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Utils.URIEncoding do
|
||||
@moduledoc """
|
||||
Utility functions for dealing with URI encoding of paths and queries
|
||||
with support for query-encoding quirks.
|
||||
"""
|
||||
require Pleroma.Constants
|
||||
|
||||
# We don't always want to decode the path first, like is the case in
|
||||
# Pleroma.Upload.url_from_spec/3.
|
||||
@doc """
|
||||
Wraps URI encoding/decoding functions from Elixir's standard library to fix usually unintended side-effects.
|
||||
|
||||
Supports two URL processing options in the optional 2nd argument with the default being `false`:
|
||||
|
||||
* `bypass_parse` - Bypasses `URI.parse` stage, useful when it's not desirable to parse to URL first
|
||||
before encoding it. Supports only encoding as the Path segment of a URI.
|
||||
* `bypass_decode` - Bypasses `URI.decode` stage for the Path segment of a URI. Used when a URL
|
||||
has to be double %-encoded for internal reasons.
|
||||
|
||||
Options must be specified as a Keyword with tuples with booleans, otherwise
|
||||
`{:error, :invalid_opts}` is returned. Example:
|
||||
`encode_url(url, [bypass_parse: true, bypass_decode: true])`
|
||||
"""
|
||||
@spec encode_url(String.t(), Keyword.t()) :: String.t() | {:error, :invalid_opts}
|
||||
def encode_url(url, opts \\ []) when is_binary(url) and is_list(opts) do
|
||||
bypass_parse = Keyword.get(opts, :bypass_parse, false)
|
||||
bypass_decode = Keyword.get(opts, :bypass_decode, false)
|
||||
|
||||
with true <- is_boolean(bypass_parse),
|
||||
true <- is_boolean(bypass_decode) do
|
||||
cond do
|
||||
bypass_parse ->
|
||||
encode_path(url, bypass_decode)
|
||||
|
||||
true ->
|
||||
URI.parse(url)
|
||||
|> then(fn parsed ->
|
||||
path = encode_path(parsed.path, bypass_decode)
|
||||
|
||||
query = encode_query(parsed.query, parsed.host)
|
||||
|
||||
%{parsed | path: path, query: query}
|
||||
end)
|
||||
|> URI.to_string()
|
||||
end
|
||||
else
|
||||
_ -> {:error, :invalid_opts}
|
||||
end
|
||||
end
|
||||
|
||||
defp encode_path(nil, _bypass_decode), do: nil
|
||||
|
||||
# URI.encode/2 deliberately does not encode all chars that are forbidden
|
||||
# in the path component of a URI. It only encodes chars that are forbidden
|
||||
# in the whole URI. A predicate in the 2nd argument is used to fix that here.
|
||||
# URI.encode/2 uses the predicate function to determine whether each byte
|
||||
# (in an integer representation) should be encoded or not.
|
||||
defp encode_path(path, bypass_decode) when is_binary(path) do
|
||||
path =
|
||||
cond do
|
||||
bypass_decode ->
|
||||
path
|
||||
|
||||
true ->
|
||||
URI.decode(path)
|
||||
end
|
||||
|
||||
path
|
||||
|> URI.encode(fn byte ->
|
||||
URI.char_unreserved?(byte) ||
|
||||
Enum.any?(
|
||||
Pleroma.Constants.uri_path_allowed_reserved_chars(),
|
||||
fn char ->
|
||||
char == byte
|
||||
end
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
# Order of kv pairs in query is not preserved when using URI.decode_query.
|
||||
# URI.query_decoder/2 returns a stream which so far appears to not change order.
|
||||
# Immediately switch to a list to prevent breakage for sites that expect
|
||||
# the order of query keys to be always the same.
|
||||
defp encode_query(query, host) when is_binary(query) do
|
||||
query
|
||||
|> URI.query_decoder()
|
||||
|> Enum.to_list()
|
||||
|> do_encode_query(host)
|
||||
end
|
||||
|
||||
defp encode_query(nil, _), do: nil
|
||||
|
||||
# Always uses www_form encoding.
|
||||
# Taken from Elixir's URI module.
|
||||
defp do_encode_query(enumerable, host) do
|
||||
Enum.map_join(enumerable, "&", &maybe_apply_query_quirk(&1, host))
|
||||
end
|
||||
|
||||
# https://git.pleroma.social/pleroma/pleroma/-/issues/1055
|
||||
defp maybe_apply_query_quirk({key, value}, "i.guim.co.uk" = _host) do
|
||||
case key do
|
||||
"precrop" ->
|
||||
query_encode_kv_pair({key, value}, ~c":,")
|
||||
|
||||
key ->
|
||||
query_encode_kv_pair({key, value})
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_apply_query_quirk({key, value}, _), do: query_encode_kv_pair({key, value})
|
||||
|
||||
# Taken from Elixir's URI module and modified to support quirks.
|
||||
defp query_encode_kv_pair({key, value}, rules \\ []) when is_list(rules) do
|
||||
cond do
|
||||
length(rules) > 0 ->
|
||||
# URI.encode_query/2 does not appear to follow spec and encodes all parts
|
||||
# of our URI path Constant. This appears to work outside of edge-cases
|
||||
# like The Guardian Rich Media Cards, keeping behavior same as with
|
||||
# URI.encode_query/2 unless otherwise specified via rules.
|
||||
(URI.encode_www_form(Kernel.to_string(key)) <>
|
||||
"=" <>
|
||||
URI.encode(value, fn byte ->
|
||||
URI.char_unreserved?(byte) ||
|
||||
Enum.any?(
|
||||
rules,
|
||||
fn char ->
|
||||
char == byte
|
||||
end
|
||||
)
|
||||
end))
|
||||
|> String.replace("%20", "+")
|
||||
|
||||
true ->
|
||||
URI.encode_www_form(Kernel.to_string(key)) <>
|
||||
"=" <> URI.encode_www_form(Kernel.to_string(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -502,6 +502,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
def fetch_objects_for_replies_collection(parent_ap_id, opts \\ %{}) do
|
||||
opts =
|
||||
opts
|
||||
|> Map.put(:order_asc, true)
|
||||
|> Map.put(:id_type, :integer)
|
||||
|
||||
from(o in Object,
|
||||
where:
|
||||
fragment("?->>'inReplyTo' = ?", o.data, ^parent_ap_id) and
|
||||
fragment(
|
||||
"(?->'to' \\? ?::text OR ?->'cc' \\? ?::text)",
|
||||
o.data,
|
||||
^Pleroma.Constants.as_public(),
|
||||
o.data,
|
||||
^Pleroma.Constants.as_public()
|
||||
) and
|
||||
fragment("?->>'type' <> 'Answer'", o.data),
|
||||
select: %{id: o.id, ap_id: fragment("?->>'id'", o.data)}
|
||||
)
|
||||
|> Pagination.fetch_paginated(opts, :keyset)
|
||||
end
|
||||
|
||||
@spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
|
||||
Ecto.UUID.t() | nil
|
||||
def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do
|
||||
|
|
@ -984,6 +1006,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_state(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)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_assigned_account(query, _), do: query
|
||||
|
||||
defp restrict_favorited_by(query, %{favorited_by: ap_id}) do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
|
|
@ -1452,6 +1482,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> restrict_actor(opts)
|
||||
|> restrict_type(opts)
|
||||
|> restrict_state(opts)
|
||||
|> restrict_assigned_account(opts)
|
||||
|> restrict_favorited_by(opts)
|
||||
|> restrict_blocked(restrict_blocked_opts)
|
||||
|> restrict_blockers_visibility(opts)
|
||||
|
|
@ -1572,7 +1603,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp get_actor_url(_url), do: nil
|
||||
|
||||
defp normalize_image(%{"url" => url} = data) do
|
||||
defp normalize_image(%{"url" => url} = data) when is_binary(url) do
|
||||
%{
|
||||
"type" => "Image",
|
||||
"url" => [%{"href" => url}]
|
||||
|
|
@ -1580,9 +1611,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> maybe_put_description(data)
|
||||
end
|
||||
|
||||
defp normalize_image(%{"url" => urls}) when is_list(urls) do
|
||||
url = urls |> List.first()
|
||||
|
||||
%{"url" => url}
|
||||
|> normalize_image()
|
||||
end
|
||||
|
||||
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
|
||||
defp normalize_image(_), do: nil
|
||||
|
||||
defp normalize_also_known_as(urls) when is_list(urls), do: urls
|
||||
defp normalize_also_known_as(url) when is_binary(url), do: [url]
|
||||
defp normalize_also_known_as(nil), do: []
|
||||
|
||||
defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do
|
||||
Map.put(map, "name", description)
|
||||
end
|
||||
|
|
@ -1638,44 +1680,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
show_birthday = !!birthday
|
||||
|
||||
# if WebFinger request was already done, we probably have acct, otherwise
|
||||
# we request WebFinger here
|
||||
nickname = additional[:nickname_from_acct] || generate_nickname(data)
|
||||
with {:ok, nickname} <- nickname_from_actor(data, additional) do
|
||||
{:ok,
|
||||
%{
|
||||
ap_id: data["id"],
|
||||
uri: get_actor_url(data["url"]),
|
||||
banner: normalize_image(data["image"]),
|
||||
fields: fields,
|
||||
emoji: emojis,
|
||||
is_locked: is_locked,
|
||||
is_discoverable: is_discoverable,
|
||||
invisible: invisible,
|
||||
avatar: normalize_image(data["icon"]),
|
||||
name: data["name"],
|
||||
follower_address: data["followers"],
|
||||
following_address: data["following"],
|
||||
featured_address: featured_address,
|
||||
bio: data["summary"] || "",
|
||||
actor_type: actor_type,
|
||||
also_known_as: normalize_also_known_as(data["alsoKnownAs"]),
|
||||
public_key: public_key,
|
||||
inbox: data["inbox"],
|
||||
shared_inbox: shared_inbox,
|
||||
accepts_chat_messages: accepts_chat_messages,
|
||||
birthday: birthday,
|
||||
show_birthday: show_birthday,
|
||||
pinned_objects: pinned_objects,
|
||||
nickname: nickname
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
%{
|
||||
ap_id: data["id"],
|
||||
uri: get_actor_url(data["url"]),
|
||||
banner: normalize_image(data["image"]),
|
||||
fields: fields,
|
||||
emoji: emojis,
|
||||
is_locked: is_locked,
|
||||
is_discoverable: is_discoverable,
|
||||
invisible: invisible,
|
||||
avatar: normalize_image(data["icon"]),
|
||||
name: data["name"],
|
||||
follower_address: data["followers"],
|
||||
following_address: data["following"],
|
||||
featured_address: featured_address,
|
||||
bio: data["summary"] || "",
|
||||
actor_type: actor_type,
|
||||
also_known_as: Map.get(data, "alsoKnownAs", []),
|
||||
public_key: public_key,
|
||||
inbox: data["inbox"],
|
||||
shared_inbox: shared_inbox,
|
||||
accepts_chat_messages: accepts_chat_messages,
|
||||
birthday: birthday,
|
||||
show_birthday: show_birthday,
|
||||
pinned_objects: pinned_objects,
|
||||
nickname: nickname
|
||||
}
|
||||
defp nickname_from_actor(data, additional) do
|
||||
generated = generated_nickname(data)
|
||||
|
||||
case additional[:nickname_from_acct] do
|
||||
^generated when is_binary(generated) ->
|
||||
{:ok, generated}
|
||||
|
||||
acct when is_binary(acct) ->
|
||||
with ^acct <- webfinger_nickname(data) do
|
||||
{:ok, acct}
|
||||
else
|
||||
_ -> {:error, {:webfinger_actor_mismatch, acct, data["id"]}}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:ok, generate_nickname(data)}
|
||||
end
|
||||
end
|
||||
|
||||
defp generated_nickname(%{"preferredUsername" => username, "id" => ap_id})
|
||||
when is_binary(username) and is_binary(ap_id) do
|
||||
case URI.parse(ap_id) do
|
||||
%URI{host: host} when is_binary(host) -> "#{username}@#{host}"
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp generated_nickname(_), do: nil
|
||||
|
||||
defp webfinger_nickname(data) do
|
||||
with generated when is_binary(generated) <- generated_nickname(data),
|
||||
{:ok, %{"subject" => "acct:" <> acct, "ap_id" => ap_id}} <- WebFinger.finger(generated),
|
||||
true <- ap_id == data["id"] do
|
||||
acct
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
|
||||
generated = "#{username}@#{URI.parse(data["id"]).host}"
|
||||
generated = generated_nickname(data)
|
||||
|
||||
if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
|
||||
case WebFinger.finger(generated) do
|
||||
{:ok, %{"subject" => "acct:" <> acct}} -> acct
|
||||
case webfinger_nickname(data) do
|
||||
acct when is_binary(acct) -> acct
|
||||
_ -> generated
|
||||
end
|
||||
else
|
||||
|
|
@ -1755,9 +1833,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
defp collection_private(_data), do: {:ok, true}
|
||||
|
||||
def user_data_from_user_object(data, additional \\ []) do
|
||||
with {:ok, data} <- MRF.filter(data) do
|
||||
{:ok, object_to_user_data(data, additional)}
|
||||
with {:ok, data} <- MRF.filter(data),
|
||||
{:ok, data} <- object_to_user_data(data, additional) do
|
||||
{:ok, data}
|
||||
else
|
||||
{:error, _} = e -> e
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|
||||
@federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
|
||||
|
||||
@object_replies_known_param_keys ["page", "min_id", "max_id", "since_id", "limit"]
|
||||
|
||||
plug(FederatingPlug when action in @federating_only_actions)
|
||||
|
||||
plug(
|
||||
|
|
@ -95,6 +97,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
end
|
||||
|
||||
def object_replies(%{assigns: assigns, query_params: params} = conn, _all_params) do
|
||||
object_ap_id = conn.path_info |> Enum.reverse() |> tl() |> Enum.reverse()
|
||||
object_ap_id = Endpoint.url() <> "/" <> Enum.join(object_ap_id, "/")
|
||||
|
||||
# Most other API params are converted to atoms by OpenAPISpex 3.x
|
||||
# and therefore helper functions assume atoms. For consistency,
|
||||
# also convert our params to atoms here.
|
||||
params =
|
||||
params
|
||||
|> Map.take(@object_replies_known_param_keys)
|
||||
|> Enum.into(%{}, fn {k, v} -> {String.to_existing_atom(k), v} end)
|
||||
|> Map.put(:object_ap_id, object_ap_id)
|
||||
|> Map.put(:order_asc, true)
|
||||
|> Map.put(:conn, conn)
|
||||
|
||||
with %Object{} = object <- Object.get_cached_by_ap_id(object_ap_id),
|
||||
user <- Map.get(assigns, :user, nil),
|
||||
{_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
|
||||
conn
|
||||
|> maybe_skip_cache(user)
|
||||
|> set_cache_ttl_for(object)
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(ObjectView)
|
||||
|> render("object_replies.json", render_params: params)
|
||||
else
|
||||
{:visible?, false} -> {:error, :not_found}
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def track_object_fetch(conn, nil), do: conn
|
||||
|
||||
def track_object_fetch(conn, object_id) do
|
||||
|
|
@ -257,8 +289,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|> put_view(UserView)
|
||||
|> render("activity_collection_page.json", %{
|
||||
activities: activities,
|
||||
pagination: ControllerHelper.get_pagination_fields(conn, activities),
|
||||
iri: "#{user.ap_id}/outbox"
|
||||
pagination: ControllerHelper.get_pagination_fields(conn, activities)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
@ -317,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
|
||||
Federator.incoming_ap_doc(%{
|
||||
Federator.incoming_failed_signature_ap_doc(%{
|
||||
method: conn.method,
|
||||
req_headers: conn.req_headers,
|
||||
request_path: conn.request_path,
|
||||
|
|
@ -404,8 +435,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|> put_view(UserView)
|
||||
|> render("activity_collection_page.json", %{
|
||||
activities: activities,
|
||||
pagination: ControllerHelper.get_pagination_fields(conn, activities),
|
||||
iri: "#{user.ap_id}/inbox"
|
||||
pagination: ControllerHelper.get_pagination_fields(conn, activities)
|
||||
})
|
||||
end
|
||||
|
||||
|
|
@ -482,6 +512,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
{:ok, activity}
|
||||
end
|
||||
|
||||
# We currently lack a Flag ObjectValidator since both CommonAPI and Transmogrifier
|
||||
# both send it straight to ActivityPub.flag and C2S currently has to go through
|
||||
# the normal pipeline which requires an ObjectValidator.
|
||||
# TODO: Add a Flag Activity ObjectValidator
|
||||
defp check_allowed_action(_, %{"type" => "Flag"}) do
|
||||
{:error, "Flag activities aren't currently supported in C2S"}
|
||||
end
|
||||
|
||||
# It would respond with 201 and silently fail with:
|
||||
# Could not decode featured collection at fetch #{user.ap_id} \
|
||||
# {:error, "Trying to fetch local resource"}
|
||||
defp check_allowed_action(%{ap_id: ap_id}, %{"type" => "Update", "object" => %{"id" => ap_id}}),
|
||||
do: {:error, "Updating profile is not currently supported in C2S"}
|
||||
|
||||
defp check_allowed_action(_, activity), do: {:ok, activity}
|
||||
|
||||
defp validate_visibility(%User{} = user, %{"type" => type, "object" => object} = activity) do
|
||||
with {_, %Object{} = normalized_object} <-
|
||||
{:normalize, Object.normalize(object, fetch: false)},
|
||||
{_, true} <- {:visibility, Visibility.visible_for_user?(normalized_object, user)} do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:normalize, _} ->
|
||||
if type in ["Create", "Listen"] do
|
||||
# Creating new object via C2S; user is local and authenticated
|
||||
# via the :authenticate Plug pipeline.
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, "No such object found"}
|
||||
end
|
||||
|
||||
{:visibility, _} ->
|
||||
{:forbidden, "You can't interact with this object"}
|
||||
end
|
||||
end
|
||||
|
||||
def update_outbox(
|
||||
%{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
|
||||
%{"nickname" => nickname} = params
|
||||
|
|
@ -493,6 +559,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|> Map.put("actor", actor)
|
||||
|
||||
with {:ok, params} <- fix_user_message(user, params),
|
||||
{:ok, params} <- check_allowed_action(user, params),
|
||||
{:ok, params} <- validate_visibility(user, params),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
|
||||
%Activity{data: activity_data} <- Activity.normalize(activity) do
|
||||
conn
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI.ActivityDraft
|
||||
alias Pleroma.Web.Endpoint
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
|
|
@ -64,15 +63,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
|
||||
defp add_emoji_content(data, emoji, url) do
|
||||
tag = [
|
||||
%{
|
||||
"id" => url,
|
||||
"type" => "Emoji",
|
||||
"name" => Emoji.maybe_quote(emoji),
|
||||
"icon" => %{
|
||||
"type" => "Image",
|
||||
"url" => url
|
||||
}
|
||||
}
|
||||
Emoji.build_emoji_tag({Emoji.maybe_strip_name(emoji), url})
|
||||
]
|
||||
|
||||
data
|
||||
|
|
@ -113,7 +104,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
|
||||
defp local_custom_emoji_react(data, emoji) do
|
||||
with %{file: path} = emojo <- Emoji.get(emoji) do
|
||||
url = "#{Endpoint.url()}#{path}"
|
||||
url = Emoji.local_url(path)
|
||||
add_emoji_content(data, emojo.code, url)
|
||||
else
|
||||
_ -> {:error, "Emoji does not exist"}
|
||||
|
|
@ -341,21 +332,18 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
|
||||
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
|
||||
def announce(actor, object, options \\ []) do
|
||||
public? = Keyword.get(options, :public, false)
|
||||
visibility = Keyword.get(options, :visibility, "public")
|
||||
|
||||
to =
|
||||
cond do
|
||||
actor.ap_id == Relay.ap_id() ->
|
||||
[actor.follower_address]
|
||||
|
||||
public? and Visibility.local_public?(object) ->
|
||||
[actor.follower_address, object.data["actor"], Utils.as_local_public()]
|
||||
|
||||
public? ->
|
||||
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
|
||||
|
||||
true ->
|
||||
[actor.follower_address, object.data["actor"]]
|
||||
{to, cc} =
|
||||
if actor.ap_id == Relay.ap_id() do
|
||||
{[actor.follower_address], []}
|
||||
else
|
||||
Pleroma.Web.CommonAPI.Utils.get_to_and_cc_for_visibility(
|
||||
visibility,
|
||||
actor.follower_address,
|
||||
nil,
|
||||
[object.data["actor"]]
|
||||
)
|
||||
end
|
||||
|
||||
{:ok,
|
||||
|
|
@ -364,6 +352,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
"actor" => actor.ap_id,
|
||||
"object" => object.data["id"],
|
||||
"to" => to,
|
||||
"cc" => cc,
|
||||
"context" => object.data["context"],
|
||||
"type" => "Announce",
|
||||
"published" => Utils.make_date()
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
|
|||
content =~ quote_url -> true
|
||||
# Does the content already have a .quote-inline span?
|
||||
content =~ "<span class=\"quote-inline\">" -> true
|
||||
# Does the content already have a .quote-inline p? (Mastodon)
|
||||
content =~ "<p class=\"quote-inline\">" -> true
|
||||
# No inline quote found
|
||||
true -> false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,7 +27,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
|
|||
end
|
||||
|
||||
defp fetch(url) do
|
||||
http_client_opts = Pleroma.Config.get([:media_proxy, :proxy_opts, :http], pool: :media)
|
||||
# This module uses Tesla (Pleroma.HTTP) to fetch the MediaProxy URL.
|
||||
# Redirect following is handled by Tesla middleware, so we must not enable
|
||||
# adapter-level redirect logic (Hackney can crash on relative redirects when proxied).
|
||||
http_client_opts =
|
||||
[:media_proxy, :proxy_opts, :http]
|
||||
|> Pleroma.Config.get(pool: :media)
|
||||
|> Keyword.drop([:follow_redirect, :force_redirect])
|
||||
|
||||
HTTP.get(url, [], http_client_opts)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -56,20 +56,24 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag])
|
||||
defp fix_tag(data), do: Map.drop(data, ["tag"])
|
||||
|
||||
# legacy internal *oma format
|
||||
defp fix_replies(%{"replies" => replies} = data) when is_list(replies), do: data
|
||||
|
||||
defp fix_replies(%{"replies" => %{"first" => %{"items" => replies}}} = data)
|
||||
when is_list(replies),
|
||||
do: Map.put(data, "replies", replies)
|
||||
|
||||
defp fix_replies(%{"replies" => %{"first" => %{"orderedItems" => replies}}} = data)
|
||||
when is_list(replies),
|
||||
do: Map.put(data, "replies", replies)
|
||||
|
||||
defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),
|
||||
do: Map.put(data, "replies", replies)
|
||||
|
||||
# TODO: Pleroma does not have any support for Collections at the moment.
|
||||
# If the `replies` field is not something the ObjectID validator can handle,
|
||||
# the activity/object would be rejected, which is bad behavior.
|
||||
defp fix_replies(%{"replies" => replies} = data) when not is_list(replies),
|
||||
do: Map.drop(data, ["replies"])
|
||||
defp fix_replies(%{"replies" => %{"orderedItems" => replies}} = data) when is_list(replies),
|
||||
do: Map.put(data, "replies", replies)
|
||||
|
||||
defp fix_replies(data), do: data
|
||||
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])
|
||||
|
|
|
|||
|
|
@ -20,7 +20,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
require Pleroma.Constants
|
||||
|
||||
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
|
||||
{:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback)
|
||||
# Fix as:Public/Public before ObjectID casting drops it, but keep `field_fallback`
|
||||
# semantics (only used when the field is missing).
|
||||
recipients =
|
||||
%{field => message[field] || field_fallback}
|
||||
|> Transmogrifier.fix_addressing_list(field)
|
||||
|> Transmogrifier.fix_addressing_public(field)
|
||||
|> Map.fetch!(field)
|
||||
|
||||
{:ok, data} = ObjectValidators.Recipients.cast(recipients)
|
||||
|
||||
data =
|
||||
Enum.reject(data, fn x ->
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|
|||
end
|
||||
|
||||
field(:closed, ObjectValidators.DateTime)
|
||||
field(:votersCount, :integer)
|
||||
field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
field(:nonAnonymous, :boolean)
|
||||
embeds_many(:anyOf, QuestionOptionsValidator)
|
||||
|
|
|
|||
|
|
@ -75,15 +75,40 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
|
|||
end
|
||||
end
|
||||
|
||||
# For remote Updates, verify the host is the same.
|
||||
# For remote Updates, verify the Actor is the same
|
||||
def validate_updating_rights_remote(cng) do
|
||||
with actor = get_field(cng, :actor),
|
||||
object = get_field(cng, :object),
|
||||
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
|
||||
actor_uri <- URI.parse(actor),
|
||||
object_uri <- URI.parse(object_id),
|
||||
true <- actor_uri.host == object_uri.host do
|
||||
cng
|
||||
entity <-
|
||||
Object.normalize(object_id, fetch: false) || User.get_cached_by_ap_id(object_id) do
|
||||
case entity do
|
||||
# Actor must own Object to update it
|
||||
%Object{} ->
|
||||
if actor == entity.data["actor"] do
|
||||
cng
|
||||
else
|
||||
cng
|
||||
|> add_error(:object, "Can't be updated by this actor")
|
||||
end
|
||||
|
||||
# Actor must only be allowed to update itself
|
||||
%User{} ->
|
||||
if actor == entity.ap_id do
|
||||
cng
|
||||
else
|
||||
cng
|
||||
|> add_error(:object, "Can't be updated by this actor")
|
||||
end
|
||||
|
||||
nil ->
|
||||
cng
|
||||
|> add_error(:object, "Can't be updated by this actor")
|
||||
|
||||
_ ->
|
||||
cng
|
||||
|> add_error(:object, "Update is neither for Object or Actor")
|
||||
end
|
||||
else
|
||||
_e ->
|
||||
cng
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
Determine if an activity can be represented by running it through Transmogrifier.
|
||||
"""
|
||||
def representable?(%Activity{} = activity) do
|
||||
with {:ok, _data} <- @transmogrifier_impl.prepare_outgoing(activity.data) do
|
||||
with {:ok, _data} <- @transmogrifier_impl.prepare_activity(activity.data) do
|
||||
true
|
||||
else
|
||||
_e ->
|
||||
|
|
@ -102,14 +102,14 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
Logger.debug("Federating #{ap_id} to #{inbox}")
|
||||
uri = %{path: path} = URI.parse(inbox)
|
||||
|
||||
{:ok, data} = @transmogrifier_impl.prepare_outgoing(activity.data)
|
||||
{:ok, data} = @transmogrifier_impl.prepare_activity(activity.data)
|
||||
|
||||
{actor, data} =
|
||||
with {_, false} <- {:actor_changed?, data["actor"] != activity.data["actor"]} do
|
||||
{actor, data}
|
||||
else
|
||||
{:actor_changed?, true} ->
|
||||
# If prepare_outgoing changes the actor, re-get it from the db
|
||||
# If prepare_activity changes the actor, re-get it from the db
|
||||
new_actor = User.get_cached_by_ap_id(data["actor"])
|
||||
{new_actor, data}
|
||||
end
|
||||
|
|
@ -331,17 +331,21 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
Repo.checkout(fn ->
|
||||
Enum.each([priority_inboxes, other_inboxes], fn inboxes ->
|
||||
Enum.each(inboxes, fn inbox ->
|
||||
%User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
|
||||
{%User{ap_id: ap_id}, priority} =
|
||||
get_user_with_priority(inbox, priority_recipients, recipients)
|
||||
|
||||
# Get all the recipients on the same host and add them to cc. Otherwise, a remote
|
||||
# instance would only accept a first message for the first recipient and ignore the rest.
|
||||
cc = get_cc_ap_ids(ap_id, recipients)
|
||||
|
||||
__MODULE__.enqueue_one(%{
|
||||
inbox: inbox,
|
||||
cc: cc,
|
||||
activity_id: activity.id
|
||||
})
|
||||
__MODULE__.enqueue_one(
|
||||
%{
|
||||
inbox: inbox,
|
||||
cc: cc,
|
||||
activity_id: activity.id
|
||||
},
|
||||
priority: priority
|
||||
)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -403,4 +407,15 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
end
|
||||
|
||||
def gather_nodeinfo_protocol_names, do: ["activitypub"]
|
||||
|
||||
defp get_user_with_priority(inbox, priority_recipients, recipients) do
|
||||
[{priority_recipients, 0}, {recipients, 1}]
|
||||
|> Enum.find_value(fn {recipients, priority} ->
|
||||
with %User{} = user <- Enum.find(recipients, fn actor -> actor.inbox == inbox end) do
|
||||
{user, priority}
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
@behaviour Pleroma.Web.ActivityPub.Transmogrifier.API
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
|
|
@ -23,7 +24,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Federator
|
||||
|
||||
import Ecto.Query
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
require Pleroma.Constants
|
||||
|
|
@ -104,6 +104,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bovine compatibility
|
||||
https://codeberg.org/bovine/bovine/issues/53
|
||||
"""
|
||||
def fix_addressing_public(map, field) do
|
||||
addrs = Map.get(map, field, []) |> List.wrap()
|
||||
|
||||
Map.put(
|
||||
map,
|
||||
field,
|
||||
Enum.map(addrs, fn
|
||||
"Public" -> Pleroma.Constants.as_public()
|
||||
"as:Public" -> Pleroma.Constants.as_public()
|
||||
x -> x
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
# if directMessage flag is set to true, leave the addressing alone
|
||||
def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
|
||||
do: object
|
||||
|
|
@ -161,6 +179,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> fix_addressing_list("cc")
|
||||
|> fix_addressing_list("bto")
|
||||
|> fix_addressing_list("bcc")
|
||||
|> fix_addressing_public("to")
|
||||
|> fix_addressing_public("cc")
|
||||
|> fix_addressing_public("bto")
|
||||
|> fix_addressing_public("bcc")
|
||||
|> fix_explicit_addressing(follower_collection)
|
||||
|> fix_implicit_addressing(follower_collection)
|
||||
end
|
||||
|
|
@ -740,50 +762,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def set_quote_url(obj), do: obj
|
||||
|
||||
@doc """
|
||||
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
|
||||
Based on Mastodon's ActivityPub::NoteSerializer#replies.
|
||||
Inline first page of the `replies` collection,
|
||||
containing any replies in chronological order.
|
||||
"""
|
||||
def set_replies(obj_data) do
|
||||
replies_uris =
|
||||
with limit when limit > 0 <-
|
||||
Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
|
||||
%Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
|
||||
object
|
||||
|> Object.self_replies()
|
||||
|> select([o], fragment("?->>'id'", o.data))
|
||||
|> limit(^limit)
|
||||
|> Repo.all()
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
|
||||
set_replies(obj_data, replies_uris)
|
||||
def set_replies(%{"type" => type} = obj_data)
|
||||
when type in Pleroma.Constants.status_object_types() do
|
||||
with obj_ap_id when is_binary(obj_ap_id) <- obj_data["id"],
|
||||
limit when limit > 0 <-
|
||||
Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
|
||||
collection <-
|
||||
Pleroma.Web.ActivityPub.ObjectView.render("object_replies.json", %{
|
||||
render_params: %{object_ap_id: obj_data["id"], limit: limit, skip_ap_ctx: true}
|
||||
}) do
|
||||
Map.put(obj_data, "replies", collection)
|
||||
else
|
||||
0 -> Map.put(obj_data, "replies", obj_data["id"] <> "/replies")
|
||||
_ -> obj_data
|
||||
end
|
||||
end
|
||||
|
||||
defp set_replies(obj, []) do
|
||||
obj
|
||||
def set_replies(obj_data), do: obj_data
|
||||
|
||||
defp set_voters_count(%{"voters" => [_ | _] = voters} = obj) do
|
||||
Map.merge(obj, %{"votersCount" => length(voters)})
|
||||
end
|
||||
|
||||
defp set_replies(obj, replies_uris) do
|
||||
replies_collection = %{
|
||||
"type" => "Collection",
|
||||
"items" => replies_uris
|
||||
}
|
||||
defp set_voters_count(obj), do: obj
|
||||
|
||||
Map.merge(obj, %{"replies" => replies_collection})
|
||||
end
|
||||
|
||||
def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
|
||||
items
|
||||
end
|
||||
|
||||
def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
|
||||
items
|
||||
end
|
||||
|
||||
def replies(_), do: []
|
||||
|
||||
# Prepares the object of an outgoing create activity.
|
||||
# Prepares and sanitizes the object for federation.
|
||||
def prepare_object(object) do
|
||||
object
|
||||
|> add_hashtags
|
||||
|
|
@ -795,6 +801,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> set_reply_to_uri
|
||||
|> set_quote_url
|
||||
|> set_replies
|
||||
|> set_voters_count
|
||||
|> CommonFixes.maybe_add_content_map()
|
||||
|> strip_internal_fields
|
||||
|> strip_internal_tags
|
||||
|
|
@ -824,7 +831,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
# internal -> Mastodon
|
||||
# """
|
||||
|
||||
def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
|
||||
def prepare_activity(%{"type" => activity_type, "object" => object_id} = data)
|
||||
when activity_type in ["Create", "Listen"] do
|
||||
object =
|
||||
object_id
|
||||
|
|
@ -840,7 +847,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
{:ok, data}
|
||||
end
|
||||
|
||||
def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
|
||||
def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
|
||||
when objtype in Pleroma.Constants.updatable_object_types() do
|
||||
data =
|
||||
data
|
||||
|
|
@ -851,7 +858,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
{:ok, data}
|
||||
end
|
||||
|
||||
def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
|
||||
def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
|
||||
when objtype in Pleroma.Constants.actor_types() do
|
||||
object =
|
||||
object
|
||||
|> maybe_fix_user_object()
|
||||
|> strip_internal_fields()
|
||||
|
||||
data =
|
||||
data
|
||||
|> Map.put("object", object)
|
||||
|> strip_internal_fields()
|
||||
|> Map.merge(Utils.make_json_ld_header(object))
|
||||
|> Map.delete("bcc")
|
||||
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
def prepare_activity(%{"type" => "Update", "object" => %{}} = data) do
|
||||
raise "Requested to serve an Update for non-updateable object type: #{inspect(data)}"
|
||||
end
|
||||
|
||||
def prepare_activity(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
|
||||
object =
|
||||
object_id
|
||||
|> Object.normalize(fetch: false)
|
||||
|
|
@ -874,7 +902,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
# Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
|
||||
# because of course it does.
|
||||
def prepare_outgoing(%{"type" => "Accept"} = data) do
|
||||
def prepare_activity(%{"type" => "Accept"} = data) do
|
||||
with follow_activity <- Activity.normalize(data["object"]) do
|
||||
object = %{
|
||||
"actor" => follow_activity.actor,
|
||||
|
|
@ -892,7 +920,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def prepare_outgoing(%{"type" => "Reject"} = data) do
|
||||
def prepare_activity(%{"type" => "Reject"} = data) do
|
||||
with follow_activity <- Activity.normalize(data["object"]) do
|
||||
object = %{
|
||||
"actor" => follow_activity.actor,
|
||||
|
|
@ -910,7 +938,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def prepare_outgoing(%{"type" => "Flag"} = data) do
|
||||
def prepare_activity(%{"type" => "Flag"} = data) do
|
||||
with {:ok, stripped_activity} <- Utils.strip_report_status_data(data),
|
||||
stripped_activity <- Utils.maybe_anonymize_reporter(stripped_activity),
|
||||
stripped_activity <- Map.merge(stripped_activity, Utils.make_json_ld_header()) do
|
||||
|
|
@ -918,7 +946,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def prepare_outgoing(%{"type" => _type} = data) do
|
||||
def prepare_activity(%{"type" => _type} = data) do
|
||||
data =
|
||||
data
|
||||
|> strip_internal_fields
|
||||
|
|
@ -985,32 +1013,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def take_emoji_tags(%User{emoji: emoji}) do
|
||||
emoji
|
||||
|> Map.to_list()
|
||||
|> Enum.map(&build_emoji_tag/1)
|
||||
|> Enum.map(&Emoji.build_emoji_tag/1)
|
||||
end
|
||||
|
||||
# TODO: we should probably send mtime instead of unix epoch time for updated
|
||||
def add_emoji_tags(%{"emoji" => emoji} = object) do
|
||||
tags = object["tag"] || []
|
||||
|
||||
out = Enum.map(emoji, &build_emoji_tag/1)
|
||||
out = Enum.map(emoji, &Emoji.build_emoji_tag/1)
|
||||
|
||||
Map.put(object, "tag", tags ++ out)
|
||||
end
|
||||
|
||||
def add_emoji_tags(object), do: object
|
||||
|
||||
def build_emoji_tag({name, url}) do
|
||||
url = URI.encode(url)
|
||||
|
||||
%{
|
||||
"icon" => %{"url" => "#{url}", "type" => "Image"},
|
||||
"name" => ":" <> name <> ":",
|
||||
"type" => "Emoji",
|
||||
"updated" => "1970-01-01T00:00:00Z",
|
||||
"id" => url
|
||||
}
|
||||
end
|
||||
|
||||
def set_conversation(object) do
|
||||
Map.put(object, "conversation", object["context"])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.API do
|
|||
Behaviour for the subset of Transmogrifier used by Publisher.
|
||||
"""
|
||||
|
||||
@callback prepare_outgoing(map()) :: {:ok, map()} | {:error, term()}
|
||||
@callback prepare_activity(map()) :: {:ok, map()} | {:error, term()}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -863,6 +863,34 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
|
||||
def update_report_state(_, _), do: {:error, "Unsupported state"}
|
||||
|
||||
def assign_report_to_account(%Activity{} = activity, nil = _account) do
|
||||
new_data = Map.delete(activity.data, "assigned_account")
|
||||
|
||||
activity
|
||||
|> Changeset.change(data: new_data)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def assign_report_to_account(%Activity{} = activity, account) do
|
||||
new_data = Map.put(activity.data, "assigned_account", account)
|
||||
|
||||
activity
|
||||
|> Changeset.change(data: new_data)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def assign_report_to_account(activity_ids, account) do
|
||||
activities_num = length(activity_ids)
|
||||
|
||||
from(a in Activity, where: a.id in ^activity_ids)
|
||||
|> update(set: [data: fragment("jsonb_set(data, '{assigned_account}', ?)", ^account)])
|
||||
|> Repo.update_all([])
|
||||
|> case do
|
||||
{^activities_num, _} -> :ok
|
||||
_ -> {:error, activity_ids}
|
||||
end
|
||||
end
|
||||
|
||||
def strip_report_status_data(%Activity{} = activity) do
|
||||
with {:ok, new_data} <- strip_report_status_data(activity.data) do
|
||||
{:ok, %{activity | data: new_data}}
|
||||
|
|
|
|||
59
lib/pleroma/web/activity_pub/views/collection_view_helper.ex
Normal file
59
lib/pleroma/web/activity_pub/views/collection_view_helper.ex
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# Copyright © 2025 Akkoma Authors <https://akkoma.dev/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.CollectionViewHelper do
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
def collection_page_offset(collection, iri, page, show_items \\ true, total \\ nil) do
|
||||
offset = (page - 1) * 10
|
||||
items = Enum.slice(collection, offset, 10)
|
||||
items = Enum.map(items, fn user -> user.ap_id end)
|
||||
total = total || length(collection)
|
||||
|
||||
map = %{
|
||||
"id" => "#{iri}?page=#{page}",
|
||||
"type" => "OrderedCollectionPage",
|
||||
"partOf" => iri,
|
||||
"totalItems" => total,
|
||||
"orderedItems" => if(show_items, do: items, else: [])
|
||||
}
|
||||
|
||||
if offset + 10 < total do
|
||||
Map.put(map, "next", "#{iri}?page=#{page + 1}")
|
||||
else
|
||||
map
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_omit_next(pagination, _items, nil), do: pagination
|
||||
|
||||
defp maybe_omit_next(pagination, items, limit) when is_binary(limit) do
|
||||
case Integer.parse(limit) do
|
||||
{limit, ""} -> maybe_omit_next(pagination, items, limit)
|
||||
_ -> maybe_omit_next(pagination, items, nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_omit_next(pagination, items, limit) when is_number(limit) do
|
||||
if Enum.count(items) < limit, do: Map.delete(pagination, "next"), else: pagination
|
||||
end
|
||||
|
||||
def collection_page_keyset(
|
||||
display_items,
|
||||
pagination,
|
||||
limit \\ nil,
|
||||
skip_ap_context \\ false
|
||||
) do
|
||||
%{
|
||||
"type" => "OrderedCollectionPage",
|
||||
"orderedItems" => display_items
|
||||
}
|
||||
|> Map.merge(pagination)
|
||||
|> maybe_omit_next(display_items, limit)
|
||||
|> then(fn m ->
|
||||
if skip_ap_context, do: m, else: Map.merge(m, Utils.make_json_ld_header())
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -6,7 +6,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
|||
use Pleroma.Web, :view
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.CollectionViewHelper
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ControllerHelper
|
||||
|
||||
def render("object.json", %{object: %Object{} = object}) do
|
||||
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(object.data)
|
||||
|
|
@ -15,26 +18,94 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
|||
Map.merge(base, additional)
|
||||
end
|
||||
|
||||
def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
|
||||
when activity_type in ["Create", "Listen"] do
|
||||
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data)
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
additional =
|
||||
Transmogrifier.prepare_object(activity.data)
|
||||
|> Map.put("object", Transmogrifier.prepare_object(object.data))
|
||||
|
||||
Map.merge(base, additional)
|
||||
def render("object.json", %{object: %Activity{} = activity}) do
|
||||
{:ok, ap_data} = Transmogrifier.prepare_activity(activity.data)
|
||||
ap_data
|
||||
end
|
||||
|
||||
def render("object.json", %{object: %Activity{} = activity}) do
|
||||
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data)
|
||||
object_id = Object.normalize(activity, id_only: true)
|
||||
def render("object_replies.json", %{
|
||||
conn: conn,
|
||||
render_params: %{object_ap_id: object_ap_id, page: "true"} = params
|
||||
}) do
|
||||
params = Map.put_new(params, :limit, 40)
|
||||
|
||||
additional =
|
||||
Transmogrifier.prepare_object(activity.data)
|
||||
|> Map.put("object", object_id)
|
||||
items = ActivityPub.fetch_objects_for_replies_collection(object_ap_id, params)
|
||||
display_items = map_reply_collection_items(items)
|
||||
|
||||
Map.merge(base, additional)
|
||||
pagination = ControllerHelper.get_pagination_fields(conn, items, %{}, :asc)
|
||||
|
||||
CollectionViewHelper.collection_page_keyset(display_items, pagination, params[:limit])
|
||||
end
|
||||
|
||||
def render(
|
||||
"object_replies.json",
|
||||
%{
|
||||
render_params: %{object_ap_id: object_ap_id} = params
|
||||
} = opts
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|> Map.drop([:max_id, :min_id, :since_id, :object_ap_id])
|
||||
|> Map.put_new(:limit, 40)
|
||||
|> Map.put(:total, true)
|
||||
|
||||
%{total: total, items: items} =
|
||||
ActivityPub.fetch_objects_for_replies_collection(object_ap_id, params)
|
||||
|
||||
display_items = map_reply_collection_items(items)
|
||||
|
||||
first_pagination = reply_collection_first_pagination(items, opts)
|
||||
|
||||
col_ap =
|
||||
%{
|
||||
"id" => object_ap_id <> "/replies",
|
||||
"type" => "OrderedCollection",
|
||||
"totalItems" => total
|
||||
}
|
||||
|
||||
col_ap =
|
||||
if total > 0 do
|
||||
first_page =
|
||||
CollectionViewHelper.collection_page_keyset(
|
||||
display_items,
|
||||
first_pagination,
|
||||
params[:limit],
|
||||
true
|
||||
)
|
||||
|
||||
Map.put(col_ap, "first", first_page)
|
||||
else
|
||||
col_ap
|
||||
end
|
||||
|
||||
if params[:skip_ap_ctx] do
|
||||
col_ap
|
||||
else
|
||||
Map.merge(col_ap, Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
|
||||
end
|
||||
end
|
||||
|
||||
defp map_reply_collection_items(items), do: Enum.map(items, fn %{ap_id: ap_id} -> ap_id end)
|
||||
|
||||
defp reply_collection_first_pagination(items, %{conn: %Plug.Conn{} = conn}) do
|
||||
pagination = ControllerHelper.get_pagination_fields(conn, items, %{"page" => true}, :asc)
|
||||
Map.put(pagination, "id", Phoenix.Controller.current_url(conn, %{"page" => true}))
|
||||
end
|
||||
|
||||
defp reply_collection_first_pagination(items, %{render_params: %{object_ap_id: object_ap_id}}) do
|
||||
%{
|
||||
"id" => object_ap_id <> "/replies?page=true",
|
||||
"partOf" => object_ap_id <> "/replies"
|
||||
}
|
||||
|> then(fn m ->
|
||||
case items do
|
||||
[] ->
|
||||
m
|
||||
|
||||
i ->
|
||||
next_id = object_ap_id <> "/replies?page=true&min_id=#{List.last(i)[:id]}"
|
||||
Map.put(m, "next", next_id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.CollectionViewHelper
|
||||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
|
@ -34,32 +35,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
def render("endpoints.json", _), do: %{}
|
||||
|
||||
def render("service.json", %{user: user}) do
|
||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
|
||||
endpoints = render("endpoints.json", %{user: user})
|
||||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
Map.merge(common_actor_fields(user), %{
|
||||
"type" => "Application",
|
||||
"following" => "#{user.ap_id}/following",
|
||||
"followers" => "#{user.ap_id}/followers",
|
||||
"inbox" => "#{user.ap_id}/inbox",
|
||||
"outbox" => "#{user.ap_id}/outbox",
|
||||
"name" => "Pleroma",
|
||||
"summary" =>
|
||||
"An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
|
||||
"url" => user.ap_id,
|
||||
"manuallyApprovesFollowers" => false,
|
||||
"publicKey" => %{
|
||||
"id" => "#{user.ap_id}#main-key",
|
||||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
},
|
||||
"endpoints" => endpoints,
|
||||
"invisible" => User.invisible?(user)
|
||||
}
|
||||
})
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
|
@ -76,13 +59,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
end
|
||||
|
||||
def render("user.json", %{user: user}) do
|
||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
user = User.sanitize_html(user)
|
||||
|
||||
endpoints = render("endpoints.json", %{user: user})
|
||||
|
||||
emoji_tags = Transmogrifier.take_emoji_tags(user)
|
||||
|
||||
fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
|
||||
|
|
@ -101,25 +79,9 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
do: Date.to_iso8601(user.birthday),
|
||||
else: nil
|
||||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
"type" => user.actor_type,
|
||||
"following" => "#{user.ap_id}/following",
|
||||
"followers" => "#{user.ap_id}/followers",
|
||||
"inbox" => "#{user.ap_id}/inbox",
|
||||
"outbox" => "#{user.ap_id}/outbox",
|
||||
Map.merge(common_actor_fields(user), %{
|
||||
"featured" => "#{user.ap_id}/collections/featured",
|
||||
"preferredUsername" => user.nickname,
|
||||
"name" => user.name,
|
||||
"summary" => user.bio,
|
||||
"url" => user.ap_id,
|
||||
"manuallyApprovesFollowers" => user.is_locked,
|
||||
"publicKey" => %{
|
||||
"id" => "#{user.ap_id}#main-key",
|
||||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
},
|
||||
"endpoints" => endpoints,
|
||||
"attachment" => fields,
|
||||
"tag" => emoji_tags,
|
||||
# Note: key name is indeed "discoverable" (not an error)
|
||||
|
|
@ -129,7 +91,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"vcard:bday" => birthday,
|
||||
"webfinger" => "acct:#{User.full_nickname(user)}",
|
||||
"published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at)
|
||||
}
|
||||
})
|
||||
|> Map.merge(
|
||||
maybe_make_image(
|
||||
&User.avatar_url/2,
|
||||
|
|
@ -164,7 +126,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
0
|
||||
end
|
||||
|
||||
collection(following, "#{user.ap_id}/following", page, showing_items, total)
|
||||
CollectionViewHelper.collection_page_offset(
|
||||
following,
|
||||
"#{user.ap_id}/following",
|
||||
page,
|
||||
showing_items,
|
||||
total
|
||||
)
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
|
@ -189,7 +157,12 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"totalItems" => total,
|
||||
"first" =>
|
||||
if showing_items do
|
||||
collection(following, "#{user.ap_id}/following", 1, !user.hide_follows)
|
||||
CollectionViewHelper.collection_page_offset(
|
||||
following,
|
||||
"#{user.ap_id}/following",
|
||||
1,
|
||||
!user.hide_follows
|
||||
)
|
||||
else
|
||||
"#{user.ap_id}/following?page=1"
|
||||
end
|
||||
|
|
@ -212,7 +185,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
0
|
||||
end
|
||||
|
||||
collection(followers, "#{user.ap_id}/followers", page, showing_items, total)
|
||||
CollectionViewHelper.collection_page_offset(
|
||||
followers,
|
||||
"#{user.ap_id}/followers",
|
||||
page,
|
||||
showing_items,
|
||||
total
|
||||
)
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
|
@ -236,7 +215,12 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"type" => "OrderedCollection",
|
||||
"first" =>
|
||||
if showing_items do
|
||||
collection(followers, "#{user.ap_id}/followers", 1, showing_items, total)
|
||||
CollectionViewHelper.collection_page_offset(
|
||||
followers,
|
||||
"#{user.ap_id}/followers",
|
||||
1,
|
||||
showing_items
|
||||
)
|
||||
else
|
||||
"#{user.ap_id}/followers?page=1"
|
||||
end
|
||||
|
|
@ -256,22 +240,15 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
|
||||
def render("activity_collection_page.json", %{
|
||||
activities: activities,
|
||||
iri: iri,
|
||||
pagination: pagination
|
||||
}) do
|
||||
collection =
|
||||
Enum.map(activities, fn activity ->
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
{:ok, data} = Transmogrifier.prepare_activity(activity.data)
|
||||
data
|
||||
end)
|
||||
|
||||
%{
|
||||
"type" => "OrderedCollectionPage",
|
||||
"partOf" => iri,
|
||||
"orderedItems" => collection
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
|> Map.merge(pagination)
|
||||
CollectionViewHelper.collection_page_keyset(collection, pagination)
|
||||
end
|
||||
|
||||
def render("featured.json", %{
|
||||
|
|
@ -293,33 +270,39 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
defp common_actor_fields(%User{} = user) do
|
||||
endpoints = render("endpoints.json", %{user: user})
|
||||
|
||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
"type" => user.actor_type,
|
||||
"following" => "#{user.ap_id}/following",
|
||||
"followers" => "#{user.ap_id}/followers",
|
||||
"inbox" => "#{user.ap_id}/inbox",
|
||||
"outbox" => "#{user.ap_id}/outbox",
|
||||
"name" => user.name,
|
||||
"summary" => user.bio,
|
||||
"url" => user.ap_id,
|
||||
"manuallyApprovesFollowers" => user.is_locked,
|
||||
"endpoints" => endpoints,
|
||||
"publicKey" => %{
|
||||
"id" => "#{user.ap_id}#main-key",
|
||||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp maybe_put_total_items(map, false, _total), do: map
|
||||
|
||||
defp maybe_put_total_items(map, true, total) do
|
||||
Map.put(map, "totalItems", total)
|
||||
end
|
||||
|
||||
def collection(collection, iri, page, show_items \\ true, total \\ nil) do
|
||||
offset = (page - 1) * 10
|
||||
items = Enum.slice(collection, offset, 10)
|
||||
items = Enum.map(items, fn user -> user.ap_id end)
|
||||
total = total || length(collection)
|
||||
|
||||
map = %{
|
||||
"id" => "#{iri}?page=#{page}",
|
||||
"type" => "OrderedCollectionPage",
|
||||
"partOf" => iri,
|
||||
"totalItems" => total,
|
||||
"orderedItems" => if(show_items, do: items, else: [])
|
||||
}
|
||||
|
||||
if offset < total do
|
||||
Map.put(map, "next", "#{iri}?page=#{page + 1}")
|
||||
else
|
||||
map
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_make_image(func, description, key, user) do
|
||||
if image = func.(user, no_default: true) do
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -73,6 +73,22 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|
|||
|> Pleroma.List.member?(user)
|
||||
end
|
||||
|
||||
def visible_for_user?(%Activity{object: %Object{} = object} = activity, nil) do
|
||||
activity_visibility? = restrict_unauthenticated_access?(activity)
|
||||
activity_public? = public?(activity) and not local_public?(activity)
|
||||
object_visibility? = restrict_unauthenticated_access?(object)
|
||||
object_public? = public?(object) and not local_public?(object)
|
||||
|
||||
# Activity could be local, but object might not (Announce/Like)
|
||||
cond do
|
||||
activity_visibility? or object_visibility? ->
|
||||
false
|
||||
|
||||
true ->
|
||||
activity_public? and object_public?
|
||||
end
|
||||
end
|
||||
|
||||
def visible_for_user?(%{__struct__: module} = message, nil)
|
||||
when module in [Activity, Object] do
|
||||
if restrict_unauthenticated_access?(message),
|
||||
|
|
|
|||
|
|
@ -174,6 +174,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
end
|
||||
end
|
||||
|
||||
defp whitelisted_config?(":pleroma", ":database_config_whitelist"), do: false
|
||||
|
||||
defp whitelisted_config?(group, key) do
|
||||
if whitelisted_configs = Config.get(:database_config_whitelist) do
|
||||
Enum.any?(whitelisted_configs, fn
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.ReportNote
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.AdminAPI
|
||||
alias Pleroma.Web.AdminAPI.Report
|
||||
|
|
@ -24,7 +25,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
|
|||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["admin:write:reports"]}
|
||||
when action in [:update, :notes_create, :notes_delete]
|
||||
when action in [:update, :assign_account, :notes_create, :notes_delete]
|
||||
)
|
||||
|
||||
action_fallback(AdminAPI.FallbackController)
|
||||
|
|
@ -79,6 +80,22 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
|
|||
end
|
||||
end
|
||||
|
||||
def assign_account(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{body_params: %{reports: reports}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
result = Enum.map(reports, &do_assign_account(&1, admin))
|
||||
|
||||
if Enum.any?(result, &Map.has_key?(&1, :error)) do
|
||||
json_response(conn, :bad_request, result)
|
||||
else
|
||||
json_response(conn, :no_content, "")
|
||||
end
|
||||
end
|
||||
|
||||
def notes_create(
|
||||
%{
|
||||
assigns: %{user: user},
|
||||
|
|
@ -131,4 +148,40 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
|
|||
_ -> json_response(conn, :bad_request, "")
|
||||
end
|
||||
end
|
||||
|
||||
defp do_assign_account(%{assigned_account: nil, id: id}, admin) 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
|
||||
})
|
||||
|
||||
activity
|
||||
else
|
||||
{:error, message} ->
|
||||
%{id: id, error: message}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_assign_account(%{assigned_account: assigned_account, id: id}, admin) do
|
||||
with %User{id: account} = user <- User.get_cached_by_nickname(assigned_account),
|
||||
{:ok, activity} <- CommonAPI.assign_report_to_account(id, account),
|
||||
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: user.nickname
|
||||
})
|
||||
|
||||
activity
|
||||
else
|
||||
{:error, message} ->
|
||||
%{id: id, error: message}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ defmodule Pleroma.Web.AdminAPI.Report do
|
|||
user = User.get_cached_by_ap_id(actor)
|
||||
account = User.get_cached_by_ap_id(account_ap_id)
|
||||
|
||||
assigned_account =
|
||||
if Map.has_key?(report.data, "assigned_account") do
|
||||
User.get_cached_by_id(report.data["assigned_account"])
|
||||
end
|
||||
|
||||
statuses =
|
||||
status_ap_ids
|
||||
|> Enum.reject(&is_nil(&1))
|
||||
|
|
@ -26,7 +31,13 @@ defmodule Pleroma.Web.AdminAPI.Report do
|
|||
Activity.get_by_ap_id_with_object(act)
|
||||
end)
|
||||
|
||||
%{report: report, user: user, account: account, statuses: statuses}
|
||||
%{
|
||||
report: report,
|
||||
user: user,
|
||||
account: account,
|
||||
statuses: statuses,
|
||||
assigned_account: assigned_account
|
||||
}
|
||||
end
|
||||
|
||||
defp make_fake_activity(act, user) do
|
||||
|
|
|
|||
|
|
@ -26,7 +26,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
|
|||
}
|
||||
end
|
||||
|
||||
def render("show.json", %{report: report, user: user, account: account, statuses: statuses}) do
|
||||
def render("show.json", %{
|
||||
report: report,
|
||||
user: user,
|
||||
account: account,
|
||||
statuses: statuses,
|
||||
assigned_account: assigned_account
|
||||
}) do
|
||||
created_at = Utils.to_masto_date(report.data["published"])
|
||||
|
||||
content =
|
||||
|
|
@ -36,6 +42,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
|
|||
nil
|
||||
end
|
||||
|
||||
assigned_account =
|
||||
if assigned_account do
|
||||
merge_account_views(assigned_account)
|
||||
end
|
||||
|
||||
%{
|
||||
id: report.id,
|
||||
account: merge_account_views(account),
|
||||
|
|
@ -49,7 +60,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
|
|||
}),
|
||||
state: report.data["state"],
|
||||
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}),
|
||||
rules: rules(Map.get(report.data, "rules", nil))
|
||||
rules: rules(Map.get(report.data, "rules", nil)),
|
||||
assigned_account: assigned_account
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"Suggestions",
|
||||
"Announcements",
|
||||
"Remote interaction",
|
||||
"Others"
|
||||
"Others",
|
||||
"Preferred frontends"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -106,7 +106,14 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
|||
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
|
||||
end
|
||||
|
||||
defp cast_and_validate(spec, operation, conn, content_type, false = _strict, cast_opts) do
|
||||
defp cast_and_validate(
|
||||
spec,
|
||||
operation,
|
||||
%Conn{} = conn,
|
||||
content_type,
|
||||
false = _strict,
|
||||
cast_opts
|
||||
) do
|
||||
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
|
||||
{:ok, conn} ->
|
||||
{:ok, conn}
|
||||
|
|
|
|||
|
|
@ -398,6 +398,28 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def endorsements_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve account information"],
|
||||
summary: "Endorsements",
|
||||
description: "Returns endorsed accounts",
|
||||
operationId: "AccountController.endorsements",
|
||||
parameters: [
|
||||
with_relationships_param(),
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"Array of Accounts",
|
||||
"application/json",
|
||||
array_of_accounts()
|
||||
),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def remove_from_followers_operation do
|
||||
%Operation{
|
||||
tags: ["Account actions"],
|
||||
|
|
@ -461,7 +483,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
security: [%{"oAuth" => ["follow", "read:mutes"]}],
|
||||
parameters: [with_relationships_param() | pagination_params()],
|
||||
responses: %{
|
||||
200 => Operation.response("Accounts", "application/json", array_of_accounts())
|
||||
200 => Operation.response("Accounts", "application/json", array_of_muted_accounts())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -475,7 +497,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
security: [%{"oAuth" => ["read:blocks"]}],
|
||||
parameters: [with_relationships_param() | pagination_params()],
|
||||
responses: %{
|
||||
200 => Operation.response("Accounts", "application/json", array_of_accounts())
|
||||
200 => Operation.response("Accounts", "application/json", array_of_blocked_accounts())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -495,16 +517,17 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", Account),
|
||||
401 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def endorsements_operation do
|
||||
def own_endorsements_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve account information"],
|
||||
summary: "Endorsements",
|
||||
operationId: "AccountController.endorsements",
|
||||
operationId: "AccountController.own_endorsements",
|
||||
description: "Returns endorsed accounts",
|
||||
security: [%{"oAuth" => ["read:accounts"]}],
|
||||
responses: %{
|
||||
|
|
@ -874,6 +897,54 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def array_of_muted_accounts do
|
||||
%Schema{
|
||||
title: "ArrayOfMutedAccounts",
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
title: "MutedAccount",
|
||||
description: "Response schema for a muted account",
|
||||
allOf: [
|
||||
Account,
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
example: [
|
||||
Account.schema().example
|
||||
|> Map.put("mute_expires_at", "2025-11-29T16:23:13Z")
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def array_of_blocked_accounts do
|
||||
%Schema{
|
||||
title: "ArrayOfBlockedAccounts",
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
title: "BlockedAccount",
|
||||
description: "Response schema for a blocked account",
|
||||
allOf: [
|
||||
Account,
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
block_expires_at: %Schema{type: :string, format: "date-time", nullable: true}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
example: [
|
||||
Account.schema().example
|
||||
|> Map.put("block_expires_at", "2025-11-29T16:23:13Z")
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_relationships do
|
||||
%Schema{
|
||||
title: "ArrayOfRelationships",
|
||||
|
|
|
|||
|
|
@ -123,7 +123,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
|
|||
name: %Schema{type: :string, description: "Application Name"},
|
||||
scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"},
|
||||
redirect_uris: %Schema{
|
||||
type: :string,
|
||||
oneOf: [
|
||||
%Schema{type: :string},
|
||||
%Schema{type: :array, items: %Schema{type: :string}}
|
||||
],
|
||||
description:
|
||||
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
|
||||
},
|
||||
|
|
@ -141,7 +144,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
|
|||
},
|
||||
example: %{
|
||||
"name" => "My App",
|
||||
"redirect_uris" => "https://myapp.com/auth/callback",
|
||||
"redirect_uris" => ["https://myapp.com/auth/callback"],
|
||||
"website" => "https://myapp.com/",
|
||||
"scopes" => ["read", "write"],
|
||||
"trusted" => true
|
||||
|
|
@ -157,7 +160,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
|
|||
name: %Schema{type: :string, description: "Application Name"},
|
||||
scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"},
|
||||
redirect_uris: %Schema{
|
||||
type: :string,
|
||||
oneOf: [
|
||||
%Schema{type: :string},
|
||||
%Schema{type: :array, items: %Schema{type: :string}}
|
||||
],
|
||||
description:
|
||||
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
|
||||
},
|
||||
|
|
@ -175,7 +181,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
|
|||
},
|
||||
example: %{
|
||||
"name" => "My App",
|
||||
"redirect_uris" => "https://myapp.com/auth/callback",
|
||||
"redirect_uris" => ["https://myapp.com/auth/callback"],
|
||||
"website" => "https://myapp.com/",
|
||||
"scopes" => ["read", "write"],
|
||||
"trusted" => true
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
:query,
|
||||
%Schema{type: :integer, default: 50},
|
||||
"Number number of log entries per page"
|
||||
),
|
||||
Operation.parameter(
|
||||
:assigned_account,
|
||||
:query,
|
||||
%Schema{type: :string},
|
||||
"Filter by assigned account ID"
|
||||
)
|
||||
| admin_api_params()
|
||||
],
|
||||
|
|
@ -103,6 +109,22 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def assign_account_operation do
|
||||
%Operation{
|
||||
tags: ["Report management"],
|
||||
summary: "Assign account to specified reports",
|
||||
operationId: "AdminAPI.ReportController.assign_account",
|
||||
security: [%{"oAuth" => ["admin:write:reports"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody: request_body("Parameters", assign_account_request(), required: true),
|
||||
responses: %{
|
||||
204 => no_content_response(),
|
||||
400 => Operation.response("Bad Request", "application/json", update_400_response()),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def notes_create_operation do
|
||||
%Operation{
|
||||
tags: ["Report management"],
|
||||
|
|
@ -186,7 +208,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
hint: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
assigned_account:
|
||||
account_admin()
|
||||
|> Map.put(:nullable, true)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -242,6 +267,34 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
}
|
||||
end
|
||||
|
||||
defp assign_account_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:reports],
|
||||
properties: %{
|
||||
reports: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{allOf: [FlakeID], description: "Required, report ID"},
|
||||
assigned_account: %Schema{
|
||||
type: :string,
|
||||
description: "User nickname",
|
||||
nullable: true
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"reports" => [
|
||||
%{"id" => "123", "assigned_account" => "pleroma"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_400_response do
|
||||
%Schema{
|
||||
type: :array,
|
||||
|
|
|
|||
|
|
@ -97,7 +97,10 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
|
|||
properties: %{
|
||||
client_name: %Schema{type: :string, description: "A name for your application."},
|
||||
redirect_uris: %Schema{
|
||||
type: :string,
|
||||
oneOf: [
|
||||
%Schema{type: :string},
|
||||
%Schema{type: :array, items: %Schema{type: :string}}
|
||||
],
|
||||
description:
|
||||
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -142,7 +142,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
|
|||
:query,
|
||||
BooleanLike.schema(),
|
||||
"Include chats from muted users"
|
||||
)
|
||||
),
|
||||
Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("The chats of the user", "application/json", chats_response())
|
||||
|
|
@ -166,7 +167,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
|
|||
:query,
|
||||
BooleanLike.schema(),
|
||||
"Include chats from muted users"
|
||||
)
|
||||
),
|
||||
Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
|
||||
| pagination_params()
|
||||
],
|
||||
responses: %{
|
||||
|
|
@ -257,6 +259,44 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def pin_operation do
|
||||
%Operation{
|
||||
tags: ["Chats"],
|
||||
summary: "Pin a chat",
|
||||
operationId: "ChatController.pin",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "The id of the chat", required: true)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("The existing chat", "application/json", Chat)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["write:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def unpin_operation do
|
||||
%Operation{
|
||||
tags: ["Chats"],
|
||||
summary: "Unpin a chat",
|
||||
operationId: "ChatController.unpin",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "The id of the chat", required: true)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("The existing chat", "application/json", Chat)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["write:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def chats_response do
|
||||
%Schema{
|
||||
title: "ChatsResponse",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
|
|||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiNotFoundError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
|
||||
|
|
@ -35,7 +36,8 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
|
|||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
operationId: "EmojiReactionController.index",
|
||||
responses: %{
|
||||
200 => array_of_reactions_response()
|
||||
200 => array_of_reactions_response(),
|
||||
404 => Operation.response("Access denied", "application/json", ApiNotFoundError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -54,7 +56,8 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
|
|||
operationId: "EmojiReactionController.create",
|
||||
responses: %{
|
||||
200 => Operation.response("Status", "application/json", Status),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiNotFoundError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do
|
|||
summary: "Retrieve follow requests",
|
||||
security: [%{"oAuth" => ["read:follows", "follow"]}],
|
||||
operationId: "FollowRequestController.index",
|
||||
parameters: pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Account", "application/json", %Schema{
|
||||
|
|
@ -62,4 +63,22 @@ defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do
|
|||
required: true
|
||||
)
|
||||
end
|
||||
|
||||
defp pagination_params do
|
||||
[
|
||||
Operation.parameter(:max_id, :query, :string, "Return items older than this ID"),
|
||||
Operation.parameter(
|
||||
:since_id,
|
||||
:query,
|
||||
:string,
|
||||
"Return the oldest items newer than this ID"
|
||||
),
|
||||
Operation.parameter(
|
||||
:limit,
|
||||
:query,
|
||||
%Schema{type: :integer, default: 20},
|
||||
"Maximum number of items to return. Will be ignored if it's more than 40"
|
||||
)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -57,6 +57,22 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def domain_blocks_operation do
|
||||
%Operation{
|
||||
tags: ["Instance misc"],
|
||||
summary: "Retrieve instance domain blocks",
|
||||
operationId: "InstanceController.domain_blocks",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"Array of domain blocks",
|
||||
"application/json",
|
||||
array_of_domain_blocks()
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def translation_languages_operation do
|
||||
%Operation{
|
||||
tags: ["Instance misc"],
|
||||
|
|
@ -326,6 +342,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
max_pinned_statuses: %Schema{
|
||||
type: :integer,
|
||||
description: "The maximum number of pinned statuses for each account."
|
||||
},
|
||||
max_profile_fields: %Schema{
|
||||
type: :integer,
|
||||
description: "The maximum number of custom profile fields allowed to be set."
|
||||
},
|
||||
profile_field_name_limit: %Schema{
|
||||
type: :integer,
|
||||
description: "The maximum size of a profile field name, in characters."
|
||||
},
|
||||
profile_field_value_limit: %Schema{
|
||||
type: :integer,
|
||||
description: "The maximum size of a profile field value, in characters."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -420,4 +448,19 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_domain_blocks do
|
||||
%Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
domain: %Schema{type: :string},
|
||||
digest: %Schema{type: :string},
|
||||
severity: %Schema{type: :string},
|
||||
comment: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
|
|||
summary: "Create a list",
|
||||
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
|
||||
operationId: "ListController.create",
|
||||
requestBody: create_update_request(),
|
||||
requestBody: create_request(),
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("List", "application/json", List),
|
||||
|
|
@ -68,7 +68,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
|
|||
description: "Change the title of a list",
|
||||
operationId: "ListController.update",
|
||||
parameters: [id_param()],
|
||||
requestBody: create_update_request(),
|
||||
requestBody: update_request(),
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("List", "application/json", List),
|
||||
|
|
@ -164,14 +164,18 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
|
|||
)
|
||||
end
|
||||
|
||||
defp create_update_request do
|
||||
defp create_request do
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
description: "POST body for creating or updating a List",
|
||||
description: "POST body for creating a List",
|
||||
type: :object,
|
||||
properties: %{
|
||||
title: %Schema{type: :string, description: "List title"}
|
||||
title: %Schema{type: :string, description: "List title"},
|
||||
exclusive: %Schema{
|
||||
type: :boolean,
|
||||
description: "Whether members of the list should be removed from the “Home” feed"
|
||||
}
|
||||
},
|
||||
required: [:title]
|
||||
},
|
||||
|
|
@ -179,6 +183,24 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
|
|||
)
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
description: "PUT body for updating a List",
|
||||
type: :object,
|
||||
properties: %{
|
||||
title: %Schema{type: :string, description: "List title"},
|
||||
exclusive: %Schema{
|
||||
type: :boolean,
|
||||
description: "Whether members of the list should be removed from the “Home” feed"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: true
|
||||
)
|
||||
end
|
||||
|
||||
defp add_remove_accounts_request(required) when is_boolean(required) do
|
||||
request_body(
|
||||
"Parameters",
|
||||
|
|
|
|||
|
|
@ -64,25 +64,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def endorsements_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve account information"],
|
||||
summary: "Endorsements",
|
||||
description: "Returns endorsed accounts",
|
||||
operationId: "PleromaAPI.AccountController.endorsements",
|
||||
parameters: [with_relationships_param(), id_param()],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"Array of Accounts",
|
||||
"application/json",
|
||||
AccountOperation.array_of_accounts()
|
||||
),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def subscribe_operation do
|
||||
%Operation{
|
||||
deprecated: true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.PleromaFollowRequestOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def outgoing_operation do
|
||||
%Operation{
|
||||
tags: ["Follow requests"],
|
||||
summary: "Retrieve outgoing follow requests",
|
||||
security: [%{"oAuth" => ["read:follows", "follow"]}],
|
||||
operationId: "PleromaFollowRequestController.outgoing",
|
||||
parameters: pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Account", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: Account,
|
||||
example: [Account.schema().example]
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp pagination_params do
|
||||
[
|
||||
Operation.parameter(:max_id, :query, :string, "Return items older than this ID"),
|
||||
Operation.parameter(
|
||||
:since_id,
|
||||
:query,
|
||||
:string,
|
||||
"Return the oldest items newer than this ID"
|
||||
),
|
||||
Operation.parameter(
|
||||
:limit,
|
||||
:query,
|
||||
%Schema{type: :integer, default: 20},
|
||||
"Maximum number of items to return. Will be ignored if it's more than 40"
|
||||
)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
defmodule Pleroma.Web.ApiSpec.PleromaFrontendSettingsOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
@spec open_api_operation(atom) :: Operation.t()
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def available_frontends_operation do
|
||||
%Operation{
|
||||
tags: ["Preferred frontends"],
|
||||
summary: "Frontend settings profiles",
|
||||
description: "List frontend setting profiles",
|
||||
operationId: "PleromaAPI.FrontendSettingsController.available_frontends",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Frontends", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :string
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_preferred_frontend_operation do
|
||||
%Operation{
|
||||
tags: ["Preferred frontends"],
|
||||
summary: "Update preferred frontend setting",
|
||||
description: "Store preferred frontend in cookies",
|
||||
operationId: "PleromaAPI.FrontendSettingsController.update_preferred_frontend",
|
||||
requestBody:
|
||||
request_body(
|
||||
"Frontend",
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:frontend_name],
|
||||
properties: %{
|
||||
frontend_name: %Schema{
|
||||
type: :string,
|
||||
description: "Frontend name"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: true
|
||||
),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Preferred frontend", "application/json", %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
frontend_name: %Schema{
|
||||
type: :string,
|
||||
description: "Frontend name"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -20,7 +20,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
|||
%Operation{
|
||||
tags: ["Scrobbles"],
|
||||
summary: "Creates a new Listen activity for an account",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
security: [%{"oAuth" => ["write:scrobbles"]}],
|
||||
operationId: "PleromaAPI.ScrobbleController.create",
|
||||
deprecated: true,
|
||||
requestBody: request_body("Parameters", create_request(), required: true),
|
||||
|
|
@ -39,7 +39,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
|||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params()
|
||||
],
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
security: [%{"oAuth" => ["read:scrobbles"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Scrobble", "application/json", %Schema{
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaStatusOperation do
|
|||
%Operation{
|
||||
tags: ["Retrieve status information"],
|
||||
summary: "Quoted by",
|
||||
description: "View quotes for a given status",
|
||||
deprecated: true,
|
||||
description: "View quotes for a given status. Use /api/v1/statuses/:id/quotes instead.",
|
||||
operationId: "PleromaAPI.StatusController.quotes",
|
||||
parameters: [id_param() | pagination_params()],
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
||||
defmodule Pleroma.Web.ApiSpec.PleromaUtilOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
|
@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
%Operation{
|
||||
tags: ["Custom emojis"],
|
||||
summary: "List all custom emojis",
|
||||
operationId: "UtilController.emoji",
|
||||
operationId: "PleromaAPI.UtilController.emoji",
|
||||
parameters: [],
|
||||
responses: %{
|
||||
200 =>
|
||||
|
|
@ -48,7 +48,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
%Operation{
|
||||
tags: ["Others"],
|
||||
summary: "Dump frontend configurations",
|
||||
operationId: "UtilController.frontend_configurations",
|
||||
operationId: "PleromaAPI.UtilController.frontend_configurations",
|
||||
parameters: [],
|
||||
responses: %{
|
||||
200 =>
|
||||
|
|
@ -70,7 +70,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
tags: ["Account credentials"],
|
||||
summary: "Change account password",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.change_password",
|
||||
operationId: "PleromaAPI.UtilController.change_password",
|
||||
requestBody: request_body("Parameters", change_password_request(), required: true),
|
||||
responses: %{
|
||||
200 =>
|
||||
|
|
@ -106,7 +106,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
tags: ["Account credentials"],
|
||||
summary: "Change account email",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.change_email",
|
||||
operationId: "PleromaAPI.UtilController.change_email",
|
||||
requestBody: request_body("Parameters", change_email_request(), required: true),
|
||||
responses: %{
|
||||
200 =>
|
||||
|
|
@ -141,7 +141,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
tags: ["Settings"],
|
||||
summary: "Update Notification Settings",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.update_notification_settings",
|
||||
operationId: "PleromaAPI.UtilController.update_notification_settings",
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:block_from_strangers,
|
||||
|
|
@ -173,7 +173,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
tags: ["Account credentials"],
|
||||
summary: "Disable Account",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.disable_account",
|
||||
operationId: "PleromaAPI.UtilController.disable_account",
|
||||
parameters: [
|
||||
Operation.parameter(:password, :query, :string, "Password")
|
||||
],
|
||||
|
|
@ -193,7 +193,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
tags: ["Account credentials"],
|
||||
summary: "Delete Account",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.delete_account",
|
||||
operationId: "PleromaAPI.UtilController.delete_account",
|
||||
parameters: [
|
||||
Operation.parameter(:password, :query, :string, "Password")
|
||||
],
|
||||
|
|
@ -212,7 +212,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
def captcha_operation do
|
||||
%Operation{
|
||||
summary: "Get a captcha",
|
||||
operationId: "UtilController.captcha",
|
||||
operationId: "PleromaAPI.UtilController.captcha",
|
||||
tags: ["Others"],
|
||||
parameters: [],
|
||||
responses: %{
|
||||
|
|
@ -226,7 +226,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
tags: ["Account credentials"],
|
||||
summary: "Move account",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.move_account",
|
||||
operationId: "PleromaAPI.UtilController.move_account",
|
||||
requestBody: request_body("Parameters", move_account_request(), required: true),
|
||||
responses: %{
|
||||
200 =>
|
||||
|
|
@ -262,7 +262,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
tags: ["Account credentials"],
|
||||
summary: "List account aliases",
|
||||
security: [%{"oAuth" => ["read:accounts"]}],
|
||||
operationId: "UtilController.list_aliases",
|
||||
operationId: "PleromaAPI.UtilController.list_aliases",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Success", "application/json", %Schema{
|
||||
|
|
@ -286,7 +286,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
tags: ["Account credentials"],
|
||||
summary: "Add an alias to this account",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.add_alias",
|
||||
operationId: "PleromaAPI.UtilController.add_alias",
|
||||
requestBody: request_body("Parameters", add_alias_request(), required: true),
|
||||
responses: %{
|
||||
200 =>
|
||||
|
|
@ -326,7 +326,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
tags: ["Account credentials"],
|
||||
summary: "Delete an alias from this account",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.delete_alias",
|
||||
operationId: "PleromaAPI.UtilController.delete_alias",
|
||||
requestBody: request_body("Parameters", delete_alias_request(), required: true),
|
||||
responses: %{
|
||||
200 =>
|
||||
|
|
@ -366,7 +366,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
tags: ["Others"],
|
||||
summary: "Quick status check on the instance",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.healthcheck",
|
||||
operationId: "PleromaAPI.UtilController.healthcheck",
|
||||
parameters: [],
|
||||
responses: %{
|
||||
200 => Operation.response("Healthy", "application/json", %Schema{type: :object}),
|
||||
|
|
@ -376,52 +376,6 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def remote_subscribe_operation do
|
||||
%Operation{
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Remote Subscribe",
|
||||
operationId: "UtilController.remote_subscribe",
|
||||
parameters: [],
|
||||
responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
|
||||
}
|
||||
end
|
||||
|
||||
def remote_interaction_operation do
|
||||
%Operation{
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Remote interaction",
|
||||
operationId: "UtilController.remote_interaction",
|
||||
requestBody: request_body("Parameters", remote_interaction_request(), required: true),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Remote interaction URL", "application/json", %Schema{type: :object})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp remote_interaction_request do
|
||||
%Schema{
|
||||
title: "RemoteInteractionRequest",
|
||||
description: "POST body for remote interaction",
|
||||
type: :object,
|
||||
required: [:ap_id, :profile],
|
||||
properties: %{
|
||||
ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"},
|
||||
profile: %Schema{type: :string, description: "Remote profile webfinger"}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_subscribe_form_operation do
|
||||
%Operation{
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Show remote subscribe form",
|
||||
operationId: "UtilController.show_subscribe_form",
|
||||
parameters: [],
|
||||
responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
|
||||
}
|
||||
end
|
||||
|
||||
defp delete_account_request do
|
||||
%Schema{
|
||||
title: "AccountDeleteRequest",
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# 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.RemoteInteractionOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def remote_interaction_operation do
|
||||
%Operation{
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Remote interaction",
|
||||
operationId: "RemoteInteractionController.remote_interaction",
|
||||
requestBody: request_body("Parameters", remote_interaction_request(), required: true),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Remote interaction URL", "application/json", %Schema{type: :object})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp remote_interaction_request do
|
||||
%Schema{
|
||||
title: "RemoteInteractionRequest",
|
||||
description: "POST body for remote interaction",
|
||||
type: :object,
|
||||
required: [:ap_id, :profile],
|
||||
properties: %{
|
||||
ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"},
|
||||
profile: %Schema{type: :string, description: "Remote profile webfinger"}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def follow_operation do
|
||||
%Operation{
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Display follow form",
|
||||
operationId: "RemoteInteractionController.follow",
|
||||
parameters: [],
|
||||
responses: %{
|
||||
200 => Operation.response("Web Page", "text/html", %Schema{type: :string}),
|
||||
302 => Operation.response("Redirect to the status page", nil, nil)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def do_follow_operation do
|
||||
%Operation{
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Perform follow activity",
|
||||
operationId: "RemoteInteractionController.do_follow",
|
||||
parameters: [],
|
||||
responses: %{
|
||||
200 => Operation.response("Web page", "text/html", %Schema{type: :string}),
|
||||
302 => Operation.response("Redirect to the account page", nil, nil)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def authorize_interaction_operation do
|
||||
%Operation{
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Authorize remote interaction",
|
||||
operationId: "RemoteInteractionController.authorize_interaction",
|
||||
parameters: [],
|
||||
responses: %{
|
||||
302 => Operation.response("Redirect to remote_interaction path", nil, nil)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_subscribe_form_operation do
|
||||
%Operation{
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Show remote subscribe form",
|
||||
operationId: "RemoteInteractionController.show_subscribe_form",
|
||||
parameters: [],
|
||||
responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})}
|
||||
}
|
||||
end
|
||||
|
||||
def remote_subscribe_operation do
|
||||
%Operation{
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Remote Subscribe",
|
||||
operationId: "RemoteInteractionController.remote_subscribe",
|
||||
parameters: [],
|
||||
responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
|
|||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiNotFoundError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
|
||||
def open_api_operation(action) do
|
||||
|
|
@ -24,7 +25,8 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
|
|||
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Report", "application/json", create_response()),
|
||||
400 => Operation.response("Report", "application/json", ApiError)
|
||||
400 => Operation.response("Report", "application/json", ApiError),
|
||||
404 => Operation.response("Report", "application/json", ApiNotFoundError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
|
|||
:query,
|
||||
%Schema{allOf: [BooleanLike], default: false},
|
||||
"Only include accounts that the user is following"
|
||||
),
|
||||
Operation.parameter(
|
||||
:capabilities,
|
||||
:query,
|
||||
%Schema{type: :array, items: %Schema{type: :string, enum: ["accepts_chat_messages"]}},
|
||||
"Only include accounts with given capabilities"
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
alias Pleroma.Web.ApiSpec.AccountOperation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiNotFoundError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Attachment
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||
|
|
@ -177,6 +178,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => status_response(),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
|
|
@ -242,14 +244,19 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
"error" => "You have already pinned the maximum number of statuses"
|
||||
}
|
||||
}),
|
||||
404 =>
|
||||
Operation.response("Not found", "application/json", %Schema{
|
||||
allOf: [ApiError],
|
||||
title: "Unprocessable Entity",
|
||||
example: %{
|
||||
"error" => "Record not found"
|
||||
404 => Operation.response("Not found", "application/json", ApiNotFoundError),
|
||||
422 =>
|
||||
Operation.response(
|
||||
"Unprocessable Entity",
|
||||
"application/json",
|
||||
%Schema{
|
||||
allOf: [ApiError],
|
||||
title: "Unprocessable Entity",
|
||||
example: %{
|
||||
"error" => "Someone else's status cannot be unpinned"
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -275,7 +282,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
}
|
||||
}),
|
||||
responses: %{
|
||||
200 => status_response()
|
||||
200 => status_response(),
|
||||
404 => Operation.response("Not found", "application/json", ApiNotFoundError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -289,7 +297,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
operationId: "StatusController.unbookmark",
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => status_response()
|
||||
200 => status_response(),
|
||||
404 => Operation.response("Not found", "application/json", ApiNotFoundError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -324,7 +333,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
],
|
||||
responses: %{
|
||||
200 => status_response(),
|
||||
400 => Operation.response("Error", "application/json", ApiError)
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Not found", "application/json", ApiNotFoundError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -340,7 +350,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => status_response(),
|
||||
400 => Operation.response("Error", "application/json", ApiError)
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiNotFoundError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -549,6 +560,27 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def quotes_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve status information"],
|
||||
summary: "Quoted by",
|
||||
description: "View quotes for a given status",
|
||||
operationId: "StatusController.quotes",
|
||||
parameters: [id_param() | pagination_params()],
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"Array of Status",
|
||||
"application/json",
|
||||
array_of_statuses()
|
||||
),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def array_of_statuses do
|
||||
%Schema{type: :array, items: Status, example: [Status.schema().example]}
|
||||
end
|
||||
|
|
@ -599,6 +631,20 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
nullable: true,
|
||||
description: "ISO 639 language code for this status."
|
||||
},
|
||||
visibility: %Schema{
|
||||
nullable: true,
|
||||
anyOf: [
|
||||
VisibilityScope,
|
||||
%Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
|
||||
],
|
||||
description:
|
||||
"Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`"
|
||||
},
|
||||
quoted_status_id: %Schema{
|
||||
nullable: true,
|
||||
allOf: [FlakeID],
|
||||
description: "ID of the status being quoted, if any"
|
||||
},
|
||||
# Pleroma-specific properties:
|
||||
preview: %Schema{
|
||||
allOf: [BooleanLike],
|
||||
|
|
@ -619,15 +665,6 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
description:
|
||||
"A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
|
||||
},
|
||||
visibility: %Schema{
|
||||
nullable: true,
|
||||
anyOf: [
|
||||
VisibilityScope,
|
||||
%Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
|
||||
],
|
||||
description:
|
||||
"Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`"
|
||||
},
|
||||
expires_in: %Schema{
|
||||
nullable: true,
|
||||
type: :integer,
|
||||
|
|
@ -643,7 +680,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
quote_id: %Schema{
|
||||
nullable: true,
|
||||
allOf: [FlakeID],
|
||||
description: "ID of the status being quoted, if any"
|
||||
description: "Deprecated in favor of `quoted_status_id`",
|
||||
deprecated: true
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ defmodule Pleroma.Web.ApiSpec.RenderError do
|
|||
def call(conn, errors) do
|
||||
errors =
|
||||
Enum.map(errors, fn
|
||||
%{name: nil, reason: :invalid_enum} = err ->
|
||||
%OpenApiSpex.Cast.Error{err | name: err.value}
|
||||
%OpenApiSpex.Cast.Error{name: nil, reason: :invalid_enum} = err ->
|
||||
%{err | name: err.value}
|
||||
|
||||
%{name: nil} = err ->
|
||||
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
|
||||
%OpenApiSpex.Cast.Error{name: nil} = err ->
|
||||
%{err | name: List.last(err.path)}
|
||||
|
||||
err ->
|
||||
err
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
acct: %Schema{type: :string},
|
||||
avatar_static: %Schema{type: :string, format: :uri},
|
||||
avatar: %Schema{type: :string, format: :uri},
|
||||
avatar_description: %Schema{type: :string},
|
||||
bot: %Schema{type: :boolean},
|
||||
created_at: %Schema{type: :string, format: "date-time"},
|
||||
display_name: %Schema{type: :string},
|
||||
|
|
@ -31,10 +32,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
following_count: %Schema{type: :integer},
|
||||
header_static: %Schema{type: :string, format: :uri},
|
||||
header: %Schema{type: :string, format: :uri},
|
||||
header_description: %Schema{type: :string},
|
||||
id: FlakeID,
|
||||
locked: %Schema{type: :boolean},
|
||||
mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
|
||||
block_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
|
||||
note: %Schema{type: :string, format: :html},
|
||||
statuses_count: %Schema{type: :integer},
|
||||
url: %Schema{type: :string, format: :uri},
|
||||
|
|
@ -113,8 +113,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
nullable: true,
|
||||
description: "Favicon image of the user's instance"
|
||||
},
|
||||
avatar_description: %Schema{type: :string},
|
||||
header_description: %Schema{type: :string}
|
||||
avatar_description: %Schema{type: :string, deprecated: true},
|
||||
header_description: %Schema{type: :string, deprecated: true}
|
||||
}
|
||||
},
|
||||
source: %Schema{
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
|
|||
requested: %Schema{type: :boolean},
|
||||
showing_reblogs: %Schema{type: :boolean},
|
||||
subscribing: %Schema{type: :boolean},
|
||||
notifying: %Schema{type: :boolean}
|
||||
notifying: %Schema{type: :boolean},
|
||||
mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
|
||||
block_expires_at: %Schema{type: :string, format: "date-time", nullable: true}
|
||||
},
|
||||
example: %{
|
||||
"blocked_by" => false,
|
||||
|
|
|
|||
19
lib/pleroma/web/api_spec/schemas/api_not_found_error.ex
Normal file
19
lib/pleroma/web/api_spec/schemas/api_not_found_error.ex
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# 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.ApiNotFoundError do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Not Found",
|
||||
description: "Response schema for 404 API errors",
|
||||
type: :object,
|
||||
properties: %{error: %Schema{type: :string}},
|
||||
example: %{
|
||||
"error" => "Record not found"
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
@ -15,12 +15,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.BookmarkFolder do
|
|||
properties: %{
|
||||
id: FlakeID,
|
||||
name: %Schema{type: :string, description: "Folder name"},
|
||||
emoji: %Schema{type: :string, description: "Folder emoji", nullable: true}
|
||||
emoji: %Schema{type: :string, description: "Folder emoji", nullable: true},
|
||||
emoji_url: %Schema{
|
||||
type: :string,
|
||||
description: "URL of the folder emoji if it's a custom emoji, null otherwise",
|
||||
nullable: true
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "9toJCu5YZW7O7gfvH6",
|
||||
"name" => "Read later",
|
||||
"emoji" => nil
|
||||
"emoji" => nil,
|
||||
"emoji_url" => nil
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
|
|||
account: %Schema{type: :object},
|
||||
unread: %Schema{type: :integer},
|
||||
last_message: ChatMessage,
|
||||
updated_at: %Schema{type: :string, format: :"date-time"}
|
||||
updated_at: %Schema{type: :string, format: :"date-time"},
|
||||
pinned: %Schema{type: :boolean}
|
||||
},
|
||||
example: %{
|
||||
"account" => %{
|
||||
|
|
@ -69,7 +70,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
|
|||
"id" => "1",
|
||||
"unread" => 2,
|
||||
"last_message" => ChatMessage.schema().example,
|
||||
"updated_at" => "2020-04-21T15:06:45.000Z"
|
||||
"updated_at" => "2020-04-21T15:06:45.000Z",
|
||||
"pinned" => false
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.List do
|
|||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "The internal database ID of the list"},
|
||||
title: %Schema{type: :string, description: "The user-defined title of the list"}
|
||||
title: %Schema{type: :string, description: "The user-defined title of the list"},
|
||||
exclusive: %Schema{
|
||||
type: :boolean,
|
||||
description: "Whether members of the list should be removed from the “Home” feed"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "12249",
|
||||
|
|
|
|||
|
|
@ -219,7 +219,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
},
|
||||
quotes_count: %Schema{
|
||||
type: :integer,
|
||||
description: "How many statuses quoted this status"
|
||||
deprecated: true,
|
||||
description:
|
||||
"How many statuses quoted this status. Deprecated, use `quotes_count` from parent object instead."
|
||||
},
|
||||
local: %Schema{
|
||||
type: :boolean,
|
||||
|
|
@ -259,6 +261,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
}
|
||||
},
|
||||
poll: %Schema{allOf: [Poll], nullable: true, description: "The poll attached to the status"},
|
||||
quotes_count: %Schema{
|
||||
type: :integer,
|
||||
description: "How many statuses quoted this status."
|
||||
},
|
||||
reblog: %Schema{
|
||||
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
|
||||
nullable: true,
|
||||
|
|
@ -385,6 +391,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
"quotes_count" => 0
|
||||
},
|
||||
"poll" => nil,
|
||||
"quotes_count" => 0,
|
||||
"reblog" => nil,
|
||||
"reblogged" => false,
|
||||
"reblogs_count" => 0,
|
||||
|
|
|
|||
|
|
@ -222,8 +222,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
|
||||
object = %Object{} <- Object.normalize(activity, fetch: false),
|
||||
{_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
|
||||
public = public_announce?(object, params),
|
||||
{:ok, announce, _} <- Builder.announce(user, object, public: public),
|
||||
visibility = announce_visibility(object, params),
|
||||
{:ok, announce, _} <- Builder.announce(user, object, visibility: visibility),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
@ -269,6 +269,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
defp favorite_helper(user, id) do
|
||||
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
|
||||
{_, true} <- {:visibility_error, activity_visible_to_actor(object, user)},
|
||||
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
|
||||
{_, {:ok, %Activity{} = activity, _meta}} <-
|
||||
{:common_pipeline,
|
||||
|
|
@ -278,6 +279,9 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
{:find_object, _} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:visibility_error, _} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
|
||||
if {:object, {"already liked by this actor", []}} in changeset.errors do
|
||||
{:ok, :already_liked}
|
||||
|
|
@ -296,6 +300,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
||||
{:find_activity, Activity.get_by_id(id)},
|
||||
%Object{} = note <- Object.normalize(activity, fetch: false),
|
||||
{_, true} <- {:visibility_error, activity_visible_to_actor(note, user)},
|
||||
%Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
|
||||
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(like)},
|
||||
{:ok, undo, _} <- Builder.undo(user, like),
|
||||
|
|
@ -303,6 +308,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
{:ok, activity}
|
||||
else
|
||||
{:find_activity, _} -> {:error, :not_found}
|
||||
{:visibility_error, _} -> {:error, :not_found}
|
||||
_ -> {:error, dgettext("errors", "Could not unfavorite")}
|
||||
end
|
||||
end
|
||||
|
|
@ -311,11 +317,15 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
{:ok, Activity.t()} | {:error, String.t()}
|
||||
def react_with_emoji(id, user, emoji) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
{_, true} <- {:visibility_error, activity_visible_to_actor(activity, user)},
|
||||
object <- Object.normalize(activity, fetch: false),
|
||||
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:visibility_error, _} ->
|
||||
{:error, :not_found}
|
||||
|
||||
_ ->
|
||||
{:error, dgettext("errors", "Could not add reaction emoji")}
|
||||
end
|
||||
|
|
@ -397,13 +407,11 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
defp public_announce?(_, %{visibility: visibility})
|
||||
when visibility in ~w{public unlisted private direct},
|
||||
do: visibility in ~w(public unlisted)
|
||||
def announce_visibility(_, %{visibility: visibility})
|
||||
when visibility in ~w{public unlisted private direct local},
|
||||
do: visibility
|
||||
|
||||
defp public_announce?(object, _) do
|
||||
Visibility.public?(object)
|
||||
end
|
||||
def announce_visibility(object, _), do: Visibility.get_visibility(object)
|
||||
|
||||
@spec get_visibility(map(), map() | nil, Participation.t() | nil) ::
|
||||
{String.t() | nil, String.t() | nil}
|
||||
|
|
@ -506,6 +514,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
|
||||
def pin(id, %User{} = user) do
|
||||
with %Activity{} = activity <- create_activity_by_id(id),
|
||||
true <- activity_visible_to_actor(activity, user),
|
||||
true <- activity_belongs_to_actor(activity, user.ap_id),
|
||||
true <- object_type_is_allowed_for_pin(activity.object),
|
||||
true <- activity_is_public(activity),
|
||||
|
|
@ -531,6 +540,14 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
|
||||
defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
|
||||
|
||||
defp activity_visible_to_actor(activity, %User{} = user) do
|
||||
if Visibility.visible_for_user?(activity, user) do
|
||||
true
|
||||
else
|
||||
{:error, :visibility_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
|
||||
with false <- type in ["Note", "Article", "Question"] do
|
||||
{:error, :not_allowed}
|
||||
|
|
@ -539,13 +556,18 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
defp activity_is_public(activity) do
|
||||
with false <- Visibility.public?(activity) do
|
||||
{:error, :visibility_error}
|
||||
{:error, :non_public_error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
|
||||
def unpin(id, user) do
|
||||
# Order of visibility/belonging matters for MastoAPI responses.
|
||||
# post not visible -> 404
|
||||
# post visible, not owned -> 422
|
||||
with %Activity{} = activity <- create_activity_by_id(id),
|
||||
true <- activity_visible_to_actor(activity, user),
|
||||
true <- activity_belongs_to_actor(activity, user.ap_id),
|
||||
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
|
||||
{:ok, _unpin, _} <-
|
||||
Pipeline.common_pipeline(unpin_data,
|
||||
|
|
@ -562,7 +584,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
def add_mute(activity, user, params \\ %{}) do
|
||||
expires_in = Map.get(params, :expires_in, 0)
|
||||
|
||||
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
|
||||
with true <- activity_visible_to_actor(activity, user),
|
||||
{:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
|
||||
_ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
|
||||
if expires_in > 0 do
|
||||
Pleroma.Workers.MuteExpireWorker.new(
|
||||
|
|
@ -574,14 +597,21 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, :visibility_error} -> {:error, :visibility_error}
|
||||
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
|
||||
end
|
||||
end
|
||||
|
||||
@spec remove_mute(Activity.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def remove_mute(%Activity{} = activity, %User{} = user) do
|
||||
ThreadMute.remove_mute(user.id, activity.data["context"])
|
||||
{:ok, activity}
|
||||
case activity_visible_to_actor(activity, user) do
|
||||
true ->
|
||||
ThreadMute.remove_mute(user.id, activity.data["context"])
|
||||
{:ok, activity}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@spec remove_mute(String.t(), String.t()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
|
|
@ -612,6 +642,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
with {:ok, account} <- get_reported_account(data.account_id),
|
||||
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
|
||||
{:ok, statuses} <- get_report_statuses(account, data),
|
||||
true <- check_statuses_visibility(user, statuses),
|
||||
rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do
|
||||
ActivityPub.flag(%{
|
||||
context: Utils.generate_context_id(),
|
||||
|
|
@ -622,9 +653,27 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
forward: Map.get(data, :forward, false),
|
||||
rules: rules
|
||||
})
|
||||
else
|
||||
false ->
|
||||
{:error, :visibility_error}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp check_statuses_visibility(user, statuses) when is_list(statuses) do
|
||||
visibility = for status <- statuses, do: Visibility.visible_for_user?(status, user)
|
||||
|
||||
case Enum.all?(visibility) do
|
||||
true -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
# There are no statuses associated with the report, pass!
|
||||
defp check_statuses_visibility(_, status) when status == nil, do: true
|
||||
|
||||
defp get_reported_account(account_id) do
|
||||
case User.get_cached_by_id(account_id) do
|
||||
%User{} = account -> {:ok, account}
|
||||
|
|
@ -658,6 +707,22 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def assign_report_to_account(activity_ids, user) when is_list(activity_ids) do
|
||||
case Utils.assign_report_to_account(activity_ids, user) do
|
||||
:ok -> {:ok, activity_ids}
|
||||
_ -> {:error, dgettext("errors", "Could not assign account")}
|
||||
end
|
||||
end
|
||||
|
||||
def assign_report_to_account(activity_id, user) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||
Utils.assign_report_to_account(activity, user)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
_ -> {:error, dgettext("errors", "Could not assign account")}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_activity_scope(String.t(), map()) :: {:ok, any()} | {:error, any()}
|
||||
def update_activity_scope(activity_id, opts \\ %{}) do
|
||||
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|> validate()
|
||||
end
|
||||
|
||||
defp listen_object(draft) do
|
||||
defp listen_object(%__MODULE__{} = draft) do
|
||||
object =
|
||||
draft.params
|
||||
|> Map.take([:album, :artist, :title, :length])
|
||||
|
|
@ -99,34 +99,34 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|> Map.put("cc", draft.cc)
|
||||
|> Map.put("actor", draft.user.ap_id)
|
||||
|
||||
%__MODULE__{draft | object: object}
|
||||
%{draft | object: object}
|
||||
end
|
||||
|
||||
defp put_params(draft, params) do
|
||||
defp put_params(%__MODULE__{} = draft, params) do
|
||||
params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
|
||||
%__MODULE__{draft | params: params}
|
||||
%{draft | params: params}
|
||||
end
|
||||
|
||||
defp status(%{params: %{status: status}} = draft) do
|
||||
%__MODULE__{draft | status: String.trim(status)}
|
||||
defp status(%__MODULE__{params: %{status: status}} = draft) do
|
||||
%{draft | status: String.trim(status)}
|
||||
end
|
||||
|
||||
defp summary(%{params: params} = draft) do
|
||||
%__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")}
|
||||
defp summary(%__MODULE__{params: params} = draft) do
|
||||
%{draft | summary: Map.get(params, :spoiler_text, "")}
|
||||
end
|
||||
|
||||
defp full_payload(%{status: status, summary: summary} = draft) do
|
||||
defp full_payload(%__MODULE__{status: status, summary: summary} = draft) do
|
||||
full_payload = String.trim(status <> summary)
|
||||
|
||||
case Utils.validate_character_limit(full_payload, draft.attachments) do
|
||||
:ok -> %__MODULE__{draft | full_payload: full_payload}
|
||||
:ok -> %{draft | full_payload: full_payload}
|
||||
{:error, message} -> add_error(draft, message)
|
||||
end
|
||||
end
|
||||
|
||||
defp attachments(%{params: params} = draft) do
|
||||
defp attachments(%__MODULE__{params: params} = draft) do
|
||||
attachments = Utils.attachments_from_ids(params, draft.user)
|
||||
draft = %__MODULE__{draft | attachments: attachments}
|
||||
draft = %{draft | attachments: attachments}
|
||||
|
||||
case Utils.validate_attachments_count(attachments) do
|
||||
:ok -> draft
|
||||
|
|
@ -134,56 +134,76 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
end
|
||||
end
|
||||
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
|
||||
defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: ""}} = draft), do: draft
|
||||
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: :deleted}} = draft) do
|
||||
add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
|
||||
defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft)
|
||||
when is_binary(id) do
|
||||
# If a post was deleted all its activities (except the newly added Delete) are purged too,
|
||||
# thus lookup by Create db ID will yield nil just as if it never existed in the first place.
|
||||
#
|
||||
# We allow replying to Announce here, due to a Pleroma-FE quirk where if presented with
|
||||
# an Announce id it will render it as if it was just the normal referenced post, but
|
||||
# use the Announce id for replies in the in_reply_to_id key of a POST request to
|
||||
# /api/v1/statuses, or as an :id in /api/v1/statuses/:id/*.
|
||||
# TODO: Fix this quirk in FE and remove here and other affected places
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
true <- Visibility.visible_for_user?(activity, draft.user),
|
||||
{_, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do
|
||||
%{draft | in_reply_to: activity}
|
||||
else
|
||||
nil ->
|
||||
add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
|
||||
|
||||
false ->
|
||||
add_error(draft, dgettext("errors", "Record not found"))
|
||||
|
||||
{:type, type} ->
|
||||
add_error(
|
||||
draft,
|
||||
dgettext("errors", "Can only reply to posts, not %{type} activities",
|
||||
type: inspect(type)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: id} = params} = draft) when is_binary(id) do
|
||||
activity = Activity.get_by_id(id)
|
||||
|
||||
params =
|
||||
if is_nil(activity) do
|
||||
# Deleted activities are returned as nil
|
||||
Map.put(params, :in_reply_to_status_id, :deleted)
|
||||
else
|
||||
Map.put(params, :in_reply_to_status_id, activity)
|
||||
end
|
||||
|
||||
in_reply_to(%{draft | params: params})
|
||||
end
|
||||
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do
|
||||
%__MODULE__{draft | in_reply_to: in_reply_to}
|
||||
defp in_reply_to(
|
||||
%__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft
|
||||
) do
|
||||
%{draft | in_reply_to: in_reply_to}
|
||||
end
|
||||
|
||||
defp in_reply_to(draft), do: draft
|
||||
|
||||
defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do
|
||||
defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft)
|
||||
when not_empty_string(id) do
|
||||
case Activity.get_by_id_with_object(id) do
|
||||
%Activity{} = activity ->
|
||||
%__MODULE__{draft | quote_post: activity}
|
||||
%{draft | quote_post: activity}
|
||||
|
||||
_ ->
|
||||
draft
|
||||
end
|
||||
end
|
||||
|
||||
defp quote_post(draft), do: draft
|
||||
|
||||
defp in_reply_to_conversation(draft) do
|
||||
in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
|
||||
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
|
||||
defp quote_post(%__MODULE__{params: %{quote_id: id}} = draft) when not_empty_string(id) do
|
||||
quote_post(%{draft | params: Map.put(draft.params, :quoted_status_id, id)})
|
||||
end
|
||||
|
||||
defp visibility(%{params: params} = draft) do
|
||||
defp quote_post(draft), do: draft
|
||||
|
||||
defp in_reply_to_conversation(%__MODULE__{} = draft) do
|
||||
in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
|
||||
%{draft | in_reply_to_conversation: in_reply_to_conversation}
|
||||
end
|
||||
|
||||
defp visibility(%__MODULE__{params: params} = draft) do
|
||||
case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do
|
||||
{visibility, "direct"} when visibility != "direct" ->
|
||||
add_error(draft, dgettext("errors", "The message visibility must be direct"))
|
||||
|
||||
{visibility, _} ->
|
||||
%__MODULE__{draft | visibility: visibility}
|
||||
%{draft | visibility: visibility}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -199,7 +219,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
false
|
||||
end
|
||||
|
||||
defp quoting_visibility(%{quote_post: %Activity{}} = draft) do
|
||||
defp quoting_visibility(%__MODULE__{quote_post: %Activity{}} = draft) do
|
||||
with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false),
|
||||
true <- can_quote?(draft, object, Visibility.get_visibility(object)) do
|
||||
draft
|
||||
|
|
@ -210,24 +230,24 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|
||||
defp quoting_visibility(draft), do: draft
|
||||
|
||||
defp expires_at(draft) do
|
||||
defp expires_at(%__MODULE__{} = draft) do
|
||||
case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
|
||||
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
|
||||
{:ok, expires_at} -> %{draft | expires_at: expires_at}
|
||||
{:error, message} -> add_error(draft, message)
|
||||
end
|
||||
end
|
||||
|
||||
defp poll(draft) do
|
||||
defp poll(%__MODULE__{} = draft) do
|
||||
case Utils.make_poll_data(draft.params) do
|
||||
{:ok, {poll, poll_emoji}} ->
|
||||
%__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
|
||||
%{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
|
||||
|
||||
{:error, message} ->
|
||||
add_error(draft, message)
|
||||
end
|
||||
end
|
||||
|
||||
defp content(%{mentions: mentions} = draft) do
|
||||
defp content(%__MODULE__{mentions: mentions} = draft) do
|
||||
{content_html, mentioned_users, tags} = Utils.make_content_html(draft)
|
||||
|
||||
mentioned_ap_ids =
|
||||
|
|
@ -238,25 +258,25 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|> Kernel.++(mentioned_ap_ids)
|
||||
|> Utils.get_addressed_users(draft.params[:to])
|
||||
|
||||
%__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
|
||||
%{draft | content_html: content_html, mentions: mentions, tags: tags}
|
||||
end
|
||||
|
||||
defp to_and_cc(draft) do
|
||||
defp to_and_cc(%__MODULE__{} = draft) do
|
||||
{to, cc} = Utils.get_to_and_cc(draft)
|
||||
%__MODULE__{draft | to: to, cc: cc}
|
||||
%{draft | to: to, cc: cc}
|
||||
end
|
||||
|
||||
defp context(draft) do
|
||||
defp context(%__MODULE__{} = draft) do
|
||||
context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation)
|
||||
%__MODULE__{draft | context: context}
|
||||
%{draft | context: context}
|
||||
end
|
||||
|
||||
defp sensitive(draft) do
|
||||
defp sensitive(%__MODULE__{} = draft) do
|
||||
sensitive = draft.params[:sensitive]
|
||||
%__MODULE__{draft | sensitive: sensitive}
|
||||
%{draft | sensitive: sensitive}
|
||||
end
|
||||
|
||||
defp language(draft) do
|
||||
defp language(%__MODULE__{} = draft) do
|
||||
language =
|
||||
with language <- draft.params[:language],
|
||||
true <- good_locale_code?(language) do
|
||||
|
|
@ -265,10 +285,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
_ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary)
|
||||
end
|
||||
|
||||
%__MODULE__{draft | language: language}
|
||||
%{draft | language: language}
|
||||
end
|
||||
|
||||
defp object(draft) do
|
||||
defp object(%__MODULE__{} = draft) do
|
||||
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
|
||||
|
||||
# Sometimes people create posts with subject containing emoji,
|
||||
|
|
@ -309,15 +329,15 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|> Map.put("generator", draft.params[:generator])
|
||||
|> Map.put("language", draft.language)
|
||||
|
||||
%__MODULE__{draft | object: object}
|
||||
%{draft | object: object}
|
||||
end
|
||||
|
||||
defp preview?(draft) do
|
||||
defp preview?(%__MODULE__{} = draft) do
|
||||
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
|
||||
%__MODULE__{draft | preview?: preview?}
|
||||
%{draft | preview?: preview?}
|
||||
end
|
||||
|
||||
defp changes(draft) do
|
||||
defp changes(%__MODULE__{} = draft) do
|
||||
direct? = draft.visibility == "direct"
|
||||
additional = %{"cc" => draft.cc, "directMessage" => direct?}
|
||||
|
||||
|
|
@ -337,14 +357,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
}
|
||||
|> Utils.maybe_add_list_data(draft.user, draft.visibility)
|
||||
|
||||
%__MODULE__{draft | changes: changes}
|
||||
%{draft | changes: changes}
|
||||
end
|
||||
|
||||
defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
|
||||
defp with_valid(draft, _func), do: draft
|
||||
|
||||
defp add_error(draft, message) do
|
||||
%__MODULE__{draft | valid?: false, errors: [message | draft.errors]}
|
||||
defp add_error(%__MODULE__{} = draft, message) do
|
||||
%{draft | valid?: false, errors: [message | draft.errors]}
|
||||
end
|
||||
|
||||
defp validate(%{valid?: true} = draft), do: {:ok, draft}
|
||||
|
|
|
|||
|
|
@ -75,48 +75,70 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
{Enum.map(participation.recipients, & &1.ap_id), []}
|
||||
end
|
||||
|
||||
def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public", "local"] do
|
||||
to =
|
||||
case visibility do
|
||||
"public" -> [Pleroma.Constants.as_public() | draft.mentions]
|
||||
"local" -> [Utils.as_local_public() | draft.mentions]
|
||||
def get_to_and_cc(%{visibility: visibility} = draft) do
|
||||
# If the OP is a DM already, add the implicit actor
|
||||
mentions =
|
||||
if visibility == "direct" && draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do
|
||||
Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions])
|
||||
else
|
||||
draft.mentions
|
||||
end
|
||||
|
||||
cc = [draft.user.follower_address]
|
||||
|
||||
if draft.in_reply_to do
|
||||
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
|
||||
else
|
||||
{to, cc}
|
||||
end
|
||||
get_to_and_cc_for_visibility(
|
||||
visibility,
|
||||
draft.user.follower_address,
|
||||
draft.in_reply_to && draft.in_reply_to.data["actor"],
|
||||
mentions
|
||||
)
|
||||
end
|
||||
|
||||
def get_to_and_cc(%{visibility: "unlisted"} = draft) do
|
||||
to = [draft.user.follower_address | draft.mentions]
|
||||
cc = [Pleroma.Constants.as_public()]
|
||||
def get_to_and_cc_for_visibility("public", follower_collection, parent_actor, mentions) do
|
||||
scope_addr = Pleroma.Constants.as_public()
|
||||
|
||||
if draft.in_reply_to do
|
||||
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
|
||||
else
|
||||
{to, cc}
|
||||
end
|
||||
to =
|
||||
if parent_actor,
|
||||
do: Enum.uniq([parent_actor, scope_addr | mentions]),
|
||||
else: [scope_addr | mentions]
|
||||
|
||||
{to, [follower_collection]}
|
||||
end
|
||||
|
||||
def get_to_and_cc(%{visibility: "private"} = draft) do
|
||||
{to, cc} = get_to_and_cc(struct(draft, visibility: "direct"))
|
||||
{[draft.user.follower_address | to], cc}
|
||||
def get_to_and_cc_for_visibility("local", follower_collection, parent_actor, mentions) do
|
||||
recipients =
|
||||
if parent_actor,
|
||||
do: Enum.uniq([parent_actor | mentions]),
|
||||
else: mentions
|
||||
|
||||
to = [
|
||||
Utils.as_local_public()
|
||||
| Enum.filter(recipients, fn addr ->
|
||||
String.starts_with?(addr, Pleroma.Web.Endpoint.url() <> "/")
|
||||
end)
|
||||
]
|
||||
|
||||
{to, [follower_collection]}
|
||||
end
|
||||
|
||||
def get_to_and_cc(%{visibility: "direct"} = draft) do
|
||||
# If the OP is a DM already, add the implicit actor.
|
||||
if draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do
|
||||
{Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []}
|
||||
else
|
||||
{draft.mentions, []}
|
||||
end
|
||||
def get_to_and_cc_for_visibility("unlisted", follower_collection, parent_actor, mentions) do
|
||||
to =
|
||||
if parent_actor,
|
||||
do: Enum.uniq([parent_actor, follower_collection | mentions]),
|
||||
else: [follower_collection | mentions]
|
||||
|
||||
{to, [Pleroma.Constants.as_public()]}
|
||||
end
|
||||
|
||||
def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []}
|
||||
def get_to_and_cc_for_visibility("private", follower_collection, _, mentions) do
|
||||
{[follower_collection | mentions], []}
|
||||
end
|
||||
|
||||
def get_to_and_cc_for_visibility("direct", _, _, mentions) do
|
||||
{mentions, []}
|
||||
end
|
||||
|
||||
def get_to_and_cc_for_visibility({:list, _}, _, _, mentions) do
|
||||
{mentions, []}
|
||||
end
|
||||
|
||||
def get_addressed_users(_, to) when is_list(to) do
|
||||
User.get_ap_ids_by_nicknames(to)
|
||||
|
|
@ -402,28 +424,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|
||||
def maybe_notify_mentioned_recipients(recipients, _), do: recipients
|
||||
|
||||
def maybe_notify_subscribers(
|
||||
recipients,
|
||||
%Activity{data: %{"actor" => actor, "type" => "Create"}} = activity
|
||||
) do
|
||||
# Do not notify subscribers if author is making a reply
|
||||
with %Object{data: object} <- Object.normalize(activity, fetch: false),
|
||||
nil <- object["inReplyTo"],
|
||||
%User{} = user <- User.get_cached_by_ap_id(actor) do
|
||||
subscriber_ids =
|
||||
user
|
||||
|> User.subscriber_users()
|
||||
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|
||||
|> Enum.map(& &1.ap_id)
|
||||
|
||||
recipients ++ subscriber_ids
|
||||
else
|
||||
_e -> recipients
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_notify_subscribers(recipients, _), do: recipients
|
||||
|
||||
def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do
|
||||
user
|
||||
|
|
@ -437,6 +437,27 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|
||||
def maybe_notify_followers(recipients, _), do: recipients
|
||||
|
||||
def get_notified_subscribers(
|
||||
%Activity{data: %{"actor" => actor, "type" => "Create"}} = activity
|
||||
) do
|
||||
# Do not notify subscribers if author is making a reply
|
||||
with %Object{data: object} <- Object.normalize(activity, fetch: false),
|
||||
nil <- object["inReplyTo"],
|
||||
%User{} = user <- User.get_cached_by_ap_id(actor) do
|
||||
subscriber_ids =
|
||||
user
|
||||
|> User.subscriber_users()
|
||||
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|
||||
|> Enum.map(& &1.ap_id)
|
||||
|
||||
subscriber_ids
|
||||
else
|
||||
_e -> []
|
||||
end
|
||||
end
|
||||
|
||||
def get_notified_subscribers(_), do: []
|
||||
|
||||
def maybe_extract_mentions(%{"tag" => tag}) do
|
||||
tag
|
||||
|> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ defmodule Pleroma.Web.ControllerHelper do
|
|||
|
||||
# TODO: Only fetch the params from open_api_spex when everything is converted
|
||||
@id_keys Pagination.page_keys() -- ["limit", "order"]
|
||||
defp build_pagination_fields(conn, min_id, max_id, extra_params) do
|
||||
defp build_pagination_fields(conn, min_id, max_id, extra_params, order) do
|
||||
params =
|
||||
if Map.has_key?(conn.private, :open_api_spex) do
|
||||
get_in(conn, [Access.key(:private), Access.key(:open_api_spex), Access.key(:params)])
|
||||
|
|
@ -66,27 +66,50 @@ defmodule Pleroma.Web.ControllerHelper do
|
|||
|> Map.merge(extra_params)
|
||||
|> Map.drop(@id_keys)
|
||||
|
||||
{{next_id, nid}, {prev_id, pid}} =
|
||||
if order == :desc,
|
||||
do: {{:max_id, max_id}, {:min_id, min_id}},
|
||||
else: {{:min_id, min_id}, {:max_id, max_id}}
|
||||
|
||||
id = Phoenix.Controller.current_url(conn)
|
||||
base_id = %{URI.parse(id) | query: nil} |> URI.to_string()
|
||||
|
||||
%{
|
||||
"next" => current_url(conn, Map.put(params, :max_id, max_id)),
|
||||
"prev" => current_url(conn, Map.put(params, :min_id, min_id)),
|
||||
"id" => current_url(conn)
|
||||
"next" => current_url(conn, Map.put(params, next_id, nid)),
|
||||
"prev" => current_url(conn, Map.put(params, prev_id, pid)),
|
||||
"id" => id,
|
||||
"partOf" => base_id
|
||||
}
|
||||
end
|
||||
|
||||
def get_pagination_fields(conn, entries, extra_params \\ %{}) do
|
||||
defp get_first_last_pagination_id(entries) do
|
||||
case List.last(entries) do
|
||||
%{pagination_id: max_id} when not is_nil(max_id) ->
|
||||
%{pagination_id: min_id} = List.first(entries)
|
||||
%{pagination_id: last_id} when not is_nil(last_id) ->
|
||||
%{pagination_id: first_id} = List.first(entries)
|
||||
{first_id, last_id}
|
||||
|
||||
build_pagination_fields(conn, min_id, max_id, extra_params)
|
||||
|
||||
%{id: max_id} ->
|
||||
%{id: min_id} = List.first(entries)
|
||||
|
||||
build_pagination_fields(conn, min_id, max_id, extra_params)
|
||||
%{id: last_id} ->
|
||||
%{id: first_id} = List.first(entries)
|
||||
{first_id, last_id}
|
||||
|
||||
_ ->
|
||||
%{}
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_pagination_fields(conn, entries, extra_params \\ %{}, order \\ :desc)
|
||||
|
||||
def get_pagination_fields(conn, entries, extra_params, :desc) do
|
||||
case get_first_last_pagination_id(entries) do
|
||||
nil -> %{}
|
||||
{min_id, max_id} -> build_pagination_fields(conn, min_id, max_id, extra_params, :desc)
|
||||
end
|
||||
end
|
||||
|
||||
def get_pagination_fields(conn, entries, extra_params, :asc) do
|
||||
case get_first_last_pagination_id(entries) do
|
||||
nil -> %{}
|
||||
{max_id, min_id} -> build_pagination_fields(conn, min_id, max_id, extra_params, :asc)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ defmodule Pleroma.Web.EmbedController do
|
|||
conn
|
||||
|> delete_resp_header("x-frame-options")
|
||||
|> delete_resp_header("content-security-policy")
|
||||
|> put_layout({Pleroma.Web.LayoutView, :embed})
|
||||
|> render("show.html",
|
||||
activity: activity,
|
||||
author: User.sanitize_html(author),
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue