Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into image-description-summary
This commit is contained in:
commit
f4c0a01f09
791 changed files with 19327 additions and 5997 deletions
|
|
@ -74,29 +74,40 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
defp check_remote_limit(_), do: true
|
||||
|
||||
def increase_note_count_if_public(actor, object) do
|
||||
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
|
||||
if public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
|
||||
end
|
||||
|
||||
def decrease_note_count_if_public(actor, object) do
|
||||
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
|
||||
if public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
|
||||
end
|
||||
|
||||
def update_last_status_at_if_public(actor, object) do
|
||||
if is_public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
|
||||
if public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
|
||||
end
|
||||
|
||||
defp increase_replies_count_if_reply(%{
|
||||
"object" => %{"inReplyTo" => reply_ap_id} = object,
|
||||
"type" => "Create"
|
||||
}) do
|
||||
if is_public?(object) do
|
||||
if public?(object) do
|
||||
Object.increase_replies_count(reply_ap_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp increase_replies_count_if_reply(_create_data), do: :noop
|
||||
|
||||
@object_types ~w[ChatMessage Question Answer Audio Video Event Article Note Page]
|
||||
defp increase_quotes_count_if_quote(%{
|
||||
"object" => %{"quoteUrl" => quote_ap_id} = object,
|
||||
"type" => "Create"
|
||||
}) do
|
||||
if public?(object) do
|
||||
Object.increase_quotes_count(quote_ap_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp increase_quotes_count_if_quote(_create_data), do: :noop
|
||||
|
||||
@object_types ~w[ChatMessage Question Answer Audio Video Image Event Article Note Page]
|
||||
@impl true
|
||||
def persist(%{"type" => type} = object, meta) when type in @object_types do
|
||||
with {:ok, object} <- Object.create(object) do
|
||||
|
|
@ -136,9 +147,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
# Splice in the child object if we have one.
|
||||
activity = Maps.put_if_present(activity, :object, object)
|
||||
|
||||
ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
|
||||
Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
|
||||
end)
|
||||
Pleroma.Web.RichMedia.Card.get_by_activity(activity)
|
||||
|
||||
# Add local posts to search index
|
||||
if local, do: Pleroma.Search.add_to_index(activity)
|
||||
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
@ -163,7 +175,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
id: "pleroma:fakeid"
|
||||
}
|
||||
|
||||
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
||||
Pleroma.Web.RichMedia.Card.get_by_activity(activity)
|
||||
{:ok, activity}
|
||||
|
||||
{:remote_limit_pass, _} ->
|
||||
|
|
@ -188,7 +200,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
|
||||
def notify_and_stream(activity) do
|
||||
Notification.create_notifications(activity)
|
||||
{:ok, notifications} = Notification.create_notifications(activity)
|
||||
Notification.send(notifications)
|
||||
|
||||
original_activity =
|
||||
case activity do
|
||||
|
|
@ -299,11 +312,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
with {:ok, activity} <- insert(create_data, local, fake),
|
||||
{:fake, false, activity} <- {:fake, fake, activity},
|
||||
_ <- increase_replies_count_if_reply(create_data),
|
||||
_ <- increase_quotes_count_if_quote(create_data),
|
||||
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
|
||||
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
||||
{:ok, _actor} <- update_last_status_at_if_public(actor, activity),
|
||||
_ <- notify_and_stream(activity),
|
||||
:ok <- maybe_schedule_poll_notifications(activity),
|
||||
:ok <- maybe_handle_group_posts(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
@ -455,6 +470,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> maybe_preload_objects(opts)
|
||||
|> maybe_preload_bookmarks(opts)
|
||||
|> maybe_set_thread_muted_field(opts)
|
||||
|> restrict_unauthenticated(opts[:user])
|
||||
|> restrict_blocked(opts)
|
||||
|> restrict_blockers_visibility(opts)
|
||||
|> restrict_recipients(recipients, opts[:user])
|
||||
|
|
@ -482,7 +498,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
|
||||
@spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
|
||||
FlakeId.Ecto.CompatType.t() | nil
|
||||
Ecto.UUID.t() | nil
|
||||
def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do
|
||||
context
|
||||
|> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts))
|
||||
|
|
@ -963,8 +979,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_replies(query, %{exclude_replies: true}) do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
where: fragment("?->>'inReplyTo' is null", object.data)
|
||||
[activity, object] in query,
|
||||
where:
|
||||
fragment("?->>'inReplyTo' is null or ?->>'type' = 'Announce'", object.data, activity.data)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -1215,6 +1232,44 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_filtered(query, _), do: query
|
||||
|
||||
defp restrict_unauthenticated(query, nil) do
|
||||
local = Config.restrict_unauthenticated_access?(:activities, :local)
|
||||
remote = Config.restrict_unauthenticated_access?(:activities, :remote)
|
||||
|
||||
cond do
|
||||
local and remote ->
|
||||
from(activity in query, where: false)
|
||||
|
||||
local ->
|
||||
from(activity in query, where: activity.local == false)
|
||||
|
||||
remote ->
|
||||
from(activity in query, where: activity.local == true)
|
||||
|
||||
true ->
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_unauthenticated(query, _), do: query
|
||||
|
||||
defp restrict_quote_url(query, %{quote_url: quote_url}) do
|
||||
from([_activity, object] in query,
|
||||
where: fragment("(?)->'quoteUrl' = ?", object.data, ^quote_url)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_quote_url(query, _), do: query
|
||||
|
||||
defp restrict_rule(query, %{rule_id: rule_id}) do
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment("(?)->'rules' \\? (?)", activity.data, ^rule_id)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_rule(query, _), do: query
|
||||
|
||||
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
|
||||
|
||||
defp exclude_poll_votes(query, _) do
|
||||
|
|
@ -1377,6 +1432,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> restrict_instance(opts)
|
||||
|> restrict_announce_object_actor(opts)
|
||||
|> restrict_filtered(opts)
|
||||
|> restrict_rule(opts)
|
||||
|> restrict_quote_url(opts)
|
||||
|> maybe_restrict_deactivated_users(opts)
|
||||
|> exclude_poll_votes(opts)
|
||||
|> exclude_chat_messages(opts)
|
||||
|
|
@ -1547,7 +1604,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
%{
|
||||
ap_id: data["id"],
|
||||
uri: get_actor_url(data["url"]),
|
||||
ap_enabled: true,
|
||||
banner: normalize_image(data["image"]),
|
||||
fields: fields,
|
||||
emoji: emojis,
|
||||
|
|
@ -1652,9 +1708,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
Fetcher.fetch_and_contain_remote_object_from_id(first) do
|
||||
{:ok, false}
|
||||
else
|
||||
{:error, {:ok, %{status: code}}} when code in [401, 403] -> {:ok, true}
|
||||
{:error, _} = e -> e
|
||||
e -> {:error, e}
|
||||
{:error, _} -> {:ok, true}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1668,7 +1722,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do
|
||||
defp fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
|
||||
{:ok, data} <- user_data_from_user_object(data, additional) do
|
||||
{:ok, maybe_update_follow_information(data)}
|
||||
|
|
@ -1721,6 +1775,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end)
|
||||
end
|
||||
|
||||
def pin_data_from_featured_collection(obj) do
|
||||
Logger.error("Could not parse featured collection #{inspect(obj)}")
|
||||
%{}
|
||||
end
|
||||
|
||||
def fetch_and_prepare_featured_from_ap_id(nil) do
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
|
@ -1751,24 +1810,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
def make_user_from_ap_id(ap_id, additional \\ []) do
|
||||
user = User.get_cached_by_ap_id(ap_id)
|
||||
|
||||
if user && !User.ap_enabled?(user) do
|
||||
Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
||||
else
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
|
||||
|
||||
if user do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
else
|
||||
maybe_handle_clashing_nickname(data)
|
||||
if user do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
else
|
||||
maybe_handle_clashing_nickname(data)
|
||||
|
||||
data
|
||||
|> User.remote_user_changeset()
|
||||
|> Repo.insert()
|
||||
|> User.set_cache()
|
||||
end
|
||||
data
|
||||
|> User.remote_user_changeset()
|
||||
|> Repo.insert()
|
||||
|> User.set_cache()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -273,12 +273,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
|
||||
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
|
||||
with %User{is_active: true} = recipient <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, %User{is_active: true} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
|
||||
true <- Utils.recipient_in_message(recipient, actor, params),
|
||||
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
|
||||
Federator.incoming_ap_doc(params)
|
||||
json(conn, "ok")
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json("Invalid request.")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -287,10 +292,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
json(conn, "ok")
|
||||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json("Invalid HTTP Signature")
|
||||
def inbox(%{assigns: %{valid_signature: false}, req_headers: req_headers} = conn, params) do
|
||||
Federator.incoming_ap_doc(%{req_headers: req_headers, params: params})
|
||||
json(conn, "ok")
|
||||
end
|
||||
|
||||
# POST /relay/inbox -or- POST /internal/fetch/inbox
|
||||
|
|
@ -476,7 +480,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|> json(message)
|
||||
|
||||
e ->
|
||||
Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
|
||||
Logger.warning(fn -> "AP C2S: #{inspect(e)}" end)
|
||||
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
This module encodes our addressing policies and general shape of our objects.
|
||||
"""
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
|
|
@ -16,6 +17,7 @@ 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
|
||||
|
||||
|
|
@ -54,13 +56,87 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
{:ok, data, []}
|
||||
end
|
||||
|
||||
defp unicode_emoji_react(_object, data, emoji) do
|
||||
data
|
||||
|> Map.put("content", emoji)
|
||||
|> Map.put("type", "EmojiReact")
|
||||
end
|
||||
|
||||
defp add_emoji_content(data, emoji, url) do
|
||||
tag = [
|
||||
%{
|
||||
"id" => url,
|
||||
"type" => "Emoji",
|
||||
"name" => Emoji.maybe_quote(emoji),
|
||||
"icon" => %{
|
||||
"type" => "Image",
|
||||
"url" => url
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
data
|
||||
|> Map.put("content", Emoji.maybe_quote(emoji))
|
||||
|> Map.put("type", "EmojiReact")
|
||||
|> Map.put("tag", tag)
|
||||
end
|
||||
|
||||
defp remote_custom_emoji_react(
|
||||
%{data: %{"reactions" => existing_reactions}},
|
||||
data,
|
||||
emoji
|
||||
) do
|
||||
[emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@")
|
||||
|
||||
matching_reaction =
|
||||
Enum.find(
|
||||
existing_reactions,
|
||||
fn [name, _, url] ->
|
||||
if url != nil do
|
||||
url = URI.parse(url)
|
||||
url.host == instance && name == emoji_code
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
if matching_reaction do
|
||||
[name, _, url] = matching_reaction
|
||||
add_emoji_content(data, name, url)
|
||||
else
|
||||
{:error, "Could not react"}
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_custom_emoji_react(_object, _data, _emoji) do
|
||||
{:error, "Could not react"}
|
||||
end
|
||||
|
||||
defp local_custom_emoji_react(data, emoji) do
|
||||
with %{file: path} = emojo <- Emoji.get(emoji) do
|
||||
url = "#{Endpoint.url()}#{path}"
|
||||
add_emoji_content(data, emojo.code, url)
|
||||
else
|
||||
_ -> {:error, "Emoji does not exist"}
|
||||
end
|
||||
end
|
||||
|
||||
defp custom_emoji_react(object, data, emoji) do
|
||||
if String.contains?(emoji, "@") do
|
||||
remote_custom_emoji_react(object, data, emoji)
|
||||
else
|
||||
local_custom_emoji_react(data, emoji)
|
||||
end
|
||||
end
|
||||
|
||||
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
||||
def emoji_react(actor, object, emoji) do
|
||||
with {:ok, data, meta} <- object_action(actor, object) do
|
||||
data =
|
||||
data
|
||||
|> Map.put("content", emoji)
|
||||
|> Map.put("type", "EmojiReact")
|
||||
if Emoji.unicode?(emoji) do
|
||||
unicode_emoji_react(object, data, emoji)
|
||||
else
|
||||
custom_emoji_react(object, data, emoji)
|
||||
end
|
||||
|
||||
{:ok, data, meta}
|
||||
end
|
||||
|
|
@ -142,6 +218,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
"tag" => Keyword.values(draft.tags) |> Enum.uniq()
|
||||
}
|
||||
|> add_in_reply_to(draft.in_reply_to)
|
||||
|> add_quote(draft.quote_post)
|
||||
|> Map.merge(draft.extra)
|
||||
|
||||
{:ok, data, []}
|
||||
|
|
@ -157,6 +234,16 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
end
|
||||
end
|
||||
|
||||
defp add_quote(object, nil), do: object
|
||||
|
||||
defp add_quote(object, quote_post) do
|
||||
with %Object{} = quote_object <- Object.normalize(quote_post, fetch: false) do
|
||||
Map.put(object, "quoteUrl", quote_object.data["id"])
|
||||
else
|
||||
_ -> object
|
||||
end
|
||||
end
|
||||
|
||||
def chat_message(actor, recipient, content, opts \\ []) do
|
||||
basic = %{
|
||||
"id" => Utils.generate_object_id(),
|
||||
|
|
@ -261,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
actor.ap_id == Relay.ap_id() ->
|
||||
[actor.follower_address]
|
||||
|
||||
public? and Visibility.is_local_public?(object) ->
|
||||
public? and Visibility.local_public?(object) ->
|
||||
[actor.follower_address, object.data["actor"], Utils.as_local_public()]
|
||||
|
||||
public? ->
|
||||
|
|
@ -289,7 +376,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
|
||||
# Address the actor of the object, and our actor's follower collection if the post is public.
|
||||
to =
|
||||
if Visibility.is_public?(object) do
|
||||
if Visibility.public?(object) do
|
||||
[actor.follower_address, object.data["actor"]]
|
||||
else
|
||||
[object.data["actor"]]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF do
|
||||
|
|
@ -54,6 +54,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
@required_description_keys [:key, :related_policy]
|
||||
|
||||
def filter_one(policy, message) do
|
||||
Code.ensure_loaded(policy)
|
||||
|
||||
should_plug_history? =
|
||||
if function_exported?(policy, :history_awareness, 0) do
|
||||
policy.history_awareness()
|
||||
|
|
@ -137,7 +139,16 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
|
||||
@spec subdomains_regex([String.t()]) :: [Regex.t()]
|
||||
def subdomains_regex(domains) when is_list(domains) do
|
||||
for domain <- domains, do: ~r(^#{String.replace(domain, "*.", "(.*\\.)*")}$)i
|
||||
for domain <- domains do
|
||||
try do
|
||||
target = String.replace(domain, "*.", "(.*\\.)*")
|
||||
~r<^#{target}$>i
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("MRF: Invalid subdomain Regex: #{domain}")
|
||||
reraise e, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec subdomain_match?([Regex.t()], String.t()) :: boolean()
|
||||
|
|
@ -188,6 +199,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
|
||||
def config_descriptions(policies) do
|
||||
Enum.reduce(policies, @mrf_config_descriptions, fn policy, acc ->
|
||||
Code.ensure_loaded(policy)
|
||||
|
||||
if function_exported?(policy, :config_description, 0) do
|
||||
description =
|
||||
@default_description
|
||||
|
|
@ -199,7 +212,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
if Enum.all?(@required_description_keys, &Map.has_key?(description, &1)) do
|
||||
[description | acc]
|
||||
else
|
||||
Logger.warn(
|
||||
Logger.warning(
|
||||
"#{policy} config description doesn't have one or all required keys #{inspect(@required_description_keys)}"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
|
|||
nick_score + name_score + actor_type_score
|
||||
end
|
||||
|
||||
defp determine_if_followbot(_), do: 0.0
|
||||
|
||||
defp bot_allowed?(%{"object" => target}, bot_actor) do
|
||||
%User{} = user = normalize_by_ap_id(target)
|
||||
|
||||
|
|
|
|||
281
lib/pleroma/web/activity_pub/mrf/emoji_policy.ex
Normal file
281
lib/pleroma/web/activity_pub/mrf/emoji_policy.ex
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Object.Updater
|
||||
alias Pleroma.Web.ActivityPub.MRF.Utils
|
||||
|
||||
@moduledoc "Reject or force-unlisted emojis with certain URLs or names"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp config_remove_url do
|
||||
Pleroma.Config.get([:mrf_emoji, :remove_url], [])
|
||||
end
|
||||
|
||||
defp config_remove_shortcode do
|
||||
Pleroma.Config.get([:mrf_emoji, :remove_shortcode], [])
|
||||
end
|
||||
|
||||
defp config_unlist_url do
|
||||
Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_url], [])
|
||||
end
|
||||
|
||||
defp config_unlist_shortcode do
|
||||
Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_shortcode], [])
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def history_awareness, do: :manual
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(%{"type" => type, "object" => %{"type" => objtype} = object} = message)
|
||||
when type in ["Create", "Update"] and objtype in Pleroma.Constants.status_object_types() do
|
||||
with {:ok, object} <-
|
||||
Updater.do_with_history(object, fn object ->
|
||||
{:ok, process_remove(object, :url, config_remove_url())}
|
||||
end),
|
||||
{:ok, object} <-
|
||||
Updater.do_with_history(object, fn object ->
|
||||
{:ok, process_remove(object, :shortcode, config_remove_shortcode())}
|
||||
end),
|
||||
activity <- Map.put(message, "object", object),
|
||||
activity <- maybe_delist(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(%{"type" => type} = object) when type in Pleroma.Constants.actor_types() do
|
||||
with object <- process_remove(object, :url, config_remove_url()),
|
||||
object <- process_remove(object, :shortcode, config_remove_shortcode()) do
|
||||
{:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(%{"type" => "EmojiReact"} = object) do
|
||||
with {:ok, _} <-
|
||||
matched_emoji_checker(config_remove_url(), config_remove_shortcode()).(object) do
|
||||
{:ok, object}
|
||||
else
|
||||
_ ->
|
||||
{:reject, "[EmojiPolicy] Rejected for having disallowed emoji"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(message) do
|
||||
{:ok, message}
|
||||
end
|
||||
|
||||
defp match_string?(string, pattern) when is_binary(pattern) do
|
||||
string == pattern
|
||||
end
|
||||
|
||||
defp match_string?(string, %Regex{} = pattern) do
|
||||
String.match?(string, pattern)
|
||||
end
|
||||
|
||||
defp match_any?(string, patterns) do
|
||||
Enum.any?(patterns, &match_string?(string, &1))
|
||||
end
|
||||
|
||||
defp url_from_tag(%{"icon" => %{"url" => url}}), do: url
|
||||
defp url_from_tag(_), do: nil
|
||||
|
||||
defp url_from_emoji({_name, url}), do: url
|
||||
|
||||
defp shortcode_from_tag(%{"name" => name}) when is_binary(name), do: String.trim(name, ":")
|
||||
defp shortcode_from_tag(_), do: nil
|
||||
|
||||
defp shortcode_from_emoji({name, _url}), do: name
|
||||
|
||||
defp process_remove(object, :url, patterns) do
|
||||
process_remove_impl(object, &url_from_tag/1, &url_from_emoji/1, patterns)
|
||||
end
|
||||
|
||||
defp process_remove(object, :shortcode, patterns) do
|
||||
process_remove_impl(object, &shortcode_from_tag/1, &shortcode_from_emoji/1, patterns)
|
||||
end
|
||||
|
||||
defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) do
|
||||
object =
|
||||
if object["tag"] do
|
||||
Map.put(
|
||||
object,
|
||||
"tag",
|
||||
Enum.filter(
|
||||
object["tag"],
|
||||
fn
|
||||
%{"type" => "Emoji"} = tag ->
|
||||
str = extract_from_tag.(tag)
|
||||
|
||||
if is_binary(str) do
|
||||
not match_any?(str, patterns)
|
||||
else
|
||||
true
|
||||
end
|
||||
|
||||
_ ->
|
||||
true
|
||||
end
|
||||
)
|
||||
)
|
||||
else
|
||||
object
|
||||
end
|
||||
|
||||
object =
|
||||
if object["emoji"] do
|
||||
Map.put(
|
||||
object,
|
||||
"emoji",
|
||||
object["emoji"]
|
||||
|> Enum.reduce(%{}, fn {name, url} = emoji, acc ->
|
||||
if not match_any?(extract_from_emoji.(emoji), patterns) do
|
||||
Map.put(acc, name, url)
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
)
|
||||
else
|
||||
object
|
||||
end
|
||||
|
||||
object
|
||||
end
|
||||
|
||||
defp matched_emoji_checker(urls, shortcodes) do
|
||||
fn object ->
|
||||
if any_emoji_match?(object, &url_from_tag/1, &url_from_emoji/1, urls) or
|
||||
any_emoji_match?(
|
||||
object,
|
||||
&shortcode_from_tag/1,
|
||||
&shortcode_from_emoji/1,
|
||||
shortcodes
|
||||
) do
|
||||
{:matched, nil}
|
||||
else
|
||||
{:ok, %{}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_delist(%{"object" => object, "to" => to, "type" => "Create"} = activity) do
|
||||
check = matched_emoji_checker(config_unlist_url(), config_unlist_shortcode())
|
||||
|
||||
should_delist? = fn object ->
|
||||
with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check) do
|
||||
false
|
||||
else
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
|
||||
if Pleroma.Constants.as_public() in to and should_delist?.(object) do
|
||||
to = List.delete(to, Pleroma.Constants.as_public())
|
||||
cc = [Pleroma.Constants.as_public() | activity["cc"] || []]
|
||||
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
else
|
||||
activity
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_delist(activity), do: activity
|
||||
|
||||
defp any_emoji_match?(object, extract_from_tag, extract_from_emoji, patterns) do
|
||||
Kernel.||(
|
||||
Enum.any?(
|
||||
object["tag"] || [],
|
||||
fn
|
||||
%{"type" => "Emoji"} = tag ->
|
||||
str = extract_from_tag.(tag)
|
||||
|
||||
if is_binary(str) do
|
||||
match_any?(str, patterns)
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
),
|
||||
(object["emoji"] || [])
|
||||
|> Enum.any?(fn emoji -> match_any?(extract_from_emoji.(emoji), patterns) end)
|
||||
)
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def describe do
|
||||
mrf_emoji =
|
||||
Pleroma.Config.get(:mrf_emoji, [])
|
||||
|> Enum.map(fn {key, value} ->
|
||||
{key, Enum.map(value, &Utils.describe_regex_or_string/1)}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{:ok, %{mrf_emoji: mrf_emoji}}
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def config_description do
|
||||
%{
|
||||
key: :mrf_emoji,
|
||||
related_policy: "Pleroma.Web.ActivityPub.MRF.EmojiPolicy",
|
||||
label: "MRF Emoji",
|
||||
description:
|
||||
"Reject or force-unlisted emojis whose URLs or names match a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
|
||||
children: [
|
||||
%{
|
||||
key: :remove_url,
|
||||
type: {:list, :string},
|
||||
description: """
|
||||
A list of patterns which result in emoji whose URL matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles.
|
||||
|
||||
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||
""",
|
||||
suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu]
|
||||
},
|
||||
%{
|
||||
key: :remove_shortcode,
|
||||
type: {:list, :string},
|
||||
description: """
|
||||
A list of patterns which result in emoji whose shortcode matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles.
|
||||
|
||||
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||
""",
|
||||
suggestions: ["foo", ~r/foo/iu]
|
||||
},
|
||||
%{
|
||||
key: :federated_timeline_removal_url,
|
||||
type: {:list, :string},
|
||||
description: """
|
||||
A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses.
|
||||
|
||||
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||
""",
|
||||
suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu]
|
||||
},
|
||||
%{
|
||||
key: :federated_timeline_removal_shortcode,
|
||||
type: {:list, :string},
|
||||
description: """
|
||||
A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses.
|
||||
|
||||
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||
""",
|
||||
suggestions: ["foo", ~r/foo/iu]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
|
|||
try_follow(follower, message)
|
||||
else
|
||||
nil ->
|
||||
Logger.warn(
|
||||
Logger.warning(
|
||||
"#{__MODULE__} skipped because of missing `:mrf_follow_bot, :follower_nickname` configuration, the :follower_nickname
|
||||
account does not exist, or the account is not correctly configured as a bot."
|
||||
)
|
||||
|
|
|
|||
59
lib/pleroma/web/activity_pub/mrf/force_mention.ex
Normal file
59
lib/pleroma/web/activity_pub/mrf/force_mention.ex
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp get_author(url) do
|
||||
with %Object{data: %{"actor" => actor}} <- Object.normalize(url, fetch: false),
|
||||
%User{ap_id: ap_id, nickname: nickname} <- User.get_cached_by_ap_id(actor) do
|
||||
%{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp prepend_author(tags, _, false), do: tags
|
||||
|
||||
defp prepend_author(tags, nil, _), do: tags
|
||||
|
||||
defp prepend_author(tags, url, _) do
|
||||
actor = get_author(url)
|
||||
|
||||
if not is_nil(actor) do
|
||||
[actor | tags]
|
||||
else
|
||||
tags
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "object" => %{"tag" => tag} = object} = activity) do
|
||||
tag =
|
||||
tag
|
||||
|> prepend_author(
|
||||
object["inReplyTo"],
|
||||
Config.get([:mrf_force_mention, :mention_parent, true])
|
||||
)
|
||||
|> prepend_author(
|
||||
object["quoteUrl"],
|
||||
Config.get([:mrf_force_mention, :mention_quoted, true])
|
||||
)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, put_in(activity["object"]["tag"], tag)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(object), do: {:ok, object}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
end
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
|
||||
|
|
@ -95,11 +95,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
|
|||
|> Enum.reject(&is_nil/1)
|
||||
|> sort_replied_user(replied_to_user)
|
||||
|
||||
explicitly_mentioned_uris = extract_mention_uris_from_content(content)
|
||||
explicitly_mentioned_uris =
|
||||
extract_mention_uris_from_content(content)
|
||||
|> MapSet.new()
|
||||
|
||||
added_mentions =
|
||||
Enum.reduce(mention_users, "", fn %User{ap_id: uri} = user, acc ->
|
||||
unless uri in explicitly_mentioned_uris do
|
||||
Enum.reduce(mention_users, "", fn %User{ap_id: ap_id, uri: uri} = user, acc ->
|
||||
if MapSet.disjoint?(MapSet.new([ap_id, uri]), explicitly_mentioned_uris) do
|
||||
acc <> Formatter.mention_from_user(user, %{mentions_format: :compact}) <> " "
|
||||
else
|
||||
acc
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
|
|||
alias Pleroma.Object
|
||||
|
||||
@moduledoc """
|
||||
Reject, TWKN-remove or Set-Sensitive messsages with specific hashtags (without the leading #)
|
||||
Reject, TWKN-remove or Set-Sensitive messages with specific hashtags (without the leading #)
|
||||
|
||||
Note: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.
|
||||
"""
|
||||
|
|
@ -84,7 +84,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
|
|||
if hashtags != [] do
|
||||
with {:ok, message} <- check_reject(message, hashtags),
|
||||
{:ok, message} <-
|
||||
(if "type" == "Create" do
|
||||
(if type == "Create" do
|
||||
check_ftl_removal(message, hashtags)
|
||||
else
|
||||
{:ok, message}
|
||||
|
|
|
|||
77
lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
Normal file
77
lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
|
||||
@moduledoc "Force a quote line into the message content."
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp build_inline_quote(template, url) do
|
||||
quote_line = String.replace(template, "{url}", "<a href=\"#{url}\">#{url}</a>")
|
||||
|
||||
"<span class=\"quote-inline\"><br/><br/>#{quote_line}</span>"
|
||||
end
|
||||
|
||||
defp has_inline_quote?(content, quote_url) do
|
||||
cond do
|
||||
# Does the quote URL exist in the content?
|
||||
content =~ quote_url -> true
|
||||
# Does the content already have a .quote-inline span?
|
||||
content =~ "<span class=\"quote-inline\">" -> true
|
||||
# No inline quote found
|
||||
true -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_object(%{"quoteUrl" => quote_url} = object) do
|
||||
content = object["content"] || ""
|
||||
|
||||
if has_inline_quote?(content, quote_url) do
|
||||
object
|
||||
else
|
||||
template = Pleroma.Config.get([:mrf_inline_quote, :template])
|
||||
|
||||
content =
|
||||
if String.ends_with?(content, "</p>"),
|
||||
do:
|
||||
String.trim_trailing(content, "</p>") <>
|
||||
build_inline_quote(template, quote_url) <> "</p>",
|
||||
else: content <> build_inline_quote(template, quote_url)
|
||||
|
||||
Map.put(object, "content", content)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do
|
||||
{:ok, Map.put(activity, "object", filter_object(object))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(object), do: {:ok, object}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def history_awareness, do: :auto
|
||||
|
||||
@impl true
|
||||
def config_description do
|
||||
%{
|
||||
key: :mrf_inline_quote,
|
||||
related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy",
|
||||
label: "MRF Inline Quote Policy",
|
||||
description: "Force quote url to appear in post content.",
|
||||
children: [
|
||||
%{
|
||||
key: :template,
|
||||
type: :string,
|
||||
description:
|
||||
"The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.",
|
||||
suggestions: ["<bdi>RT:</bdi> {url}"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -5,18 +5,17 @@
|
|||
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Web.ActivityPub.MRF.Utils
|
||||
|
||||
@moduledoc "Reject or Word-Replace messages with a keyword or regex"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
defp string_matches?(string, _) when not is_binary(string) do
|
||||
false
|
||||
end
|
||||
|
||||
defp string_matches?(string, pattern) when is_binary(pattern) do
|
||||
String.contains?(string, pattern)
|
||||
end
|
||||
|
||||
defp string_matches?(string, pattern) do
|
||||
defp string_matches?(string, %Regex{} = pattern) do
|
||||
String.match?(string, pattern)
|
||||
end
|
||||
|
||||
|
|
@ -128,7 +127,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
|
||||
@impl true
|
||||
def describe do
|
||||
# This horror is needed to convert regex sigils to strings
|
||||
mrf_keyword =
|
||||
Pleroma.Config.get(:mrf_keyword, [])
|
||||
|> Enum.map(fn {key, value} ->
|
||||
|
|
@ -136,21 +134,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
Enum.map(value, fn
|
||||
{pattern, replacement} ->
|
||||
%{
|
||||
"pattern" =>
|
||||
if not is_binary(pattern) do
|
||||
inspect(pattern)
|
||||
else
|
||||
pattern
|
||||
end,
|
||||
"pattern" => Utils.describe_regex_or_string(pattern),
|
||||
"replacement" => replacement
|
||||
}
|
||||
|
||||
pattern ->
|
||||
if not is_binary(pattern) do
|
||||
inspect(pattern)
|
||||
else
|
||||
pattern
|
||||
end
|
||||
Utils.describe_regex_or_string(pattern)
|
||||
end)}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
|
|||
|
||||
@impl true
|
||||
def filter(%{"actor" => actor} = object) do
|
||||
with true <- is_local?(actor),
|
||||
true <- is_eligible_type?(object),
|
||||
true <- is_note?(object),
|
||||
with true <- local?(actor),
|
||||
true <- eligible_type?(object),
|
||||
true <- note?(object),
|
||||
false <- has_attachment?(object),
|
||||
true <- only_mentions?(object) do
|
||||
{:reject, "[NoEmptyPolicy]"}
|
||||
|
|
@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
|
|||
|
||||
def filter(object), do: {:ok, object}
|
||||
|
||||
defp is_local?(actor) do
|
||||
defp local?(actor) do
|
||||
if actor |> String.starts_with?("#{Endpoint.url()}") do
|
||||
true
|
||||
else
|
||||
|
|
@ -59,11 +59,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
|
|||
|
||||
defp only_mentions?(_), do: false
|
||||
|
||||
defp is_note?(%{"object" => %{"type" => "Note"}}), do: true
|
||||
defp is_note?(_), do: false
|
||||
defp note?(%{"object" => %{"type" => "Note"}}), do: true
|
||||
defp note?(_), do: false
|
||||
|
||||
defp is_eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true
|
||||
defp is_eligible_type?(_), do: false
|
||||
defp eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true
|
||||
defp eligible_type?(_), do: false
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
|
|||
265
lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex
Normal file
265
lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.NsfwApiPolicy do
|
||||
@moduledoc """
|
||||
Hide, delete, or mark sensitive NSFW content with artificial intelligence.
|
||||
|
||||
Requires a NSFW API server, configured like so:
|
||||
|
||||
config :pleroma, Pleroma.Web.ActivityPub.MRF.NsfwMRF,
|
||||
url: "http://127.0.0.1:5000/",
|
||||
threshold: 0.7,
|
||||
mark_sensitive: true,
|
||||
unlist: false,
|
||||
reject: false
|
||||
|
||||
The NSFW API server must implement an HTTP endpoint like this:
|
||||
|
||||
curl http://localhost:5000/?url=https://fedi.com/images/001.jpg
|
||||
|
||||
Returning a response like this:
|
||||
|
||||
{"score", 0.314}
|
||||
|
||||
Where a score is 0-1, with `1` being definitely NSFW.
|
||||
|
||||
A good API server is here: https://github.com/EugenCepoi/nsfw_api
|
||||
You can run it with Docker with a one-liner:
|
||||
|
||||
docker run -it -p 127.0.0.1:5000:5000/tcp --env PORT=5000 eugencepoi/nsfw_api:latest
|
||||
|
||||
Options:
|
||||
|
||||
- `url`: Base URL of the API server. Default: "http://127.0.0.1:5000/"
|
||||
- `threshold`: Lowest score to take action on. Default: `0.7`
|
||||
- `mark_sensitive`: Mark sensitive all detected NSFW content? Default: `true`
|
||||
- `unlist`: Unlist all detected NSFW content? Default: `false`
|
||||
- `reject`: Reject all detected NSFW content (takes precedence)? Default: `false`
|
||||
"""
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Constants
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.User
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@policy :mrf_nsfw_api
|
||||
|
||||
def build_request_url(url) do
|
||||
Config.get([@policy, :url])
|
||||
|> URI.parse()
|
||||
|> fix_path()
|
||||
|> Map.put(:query, "url=#{url}")
|
||||
|> URI.to_string()
|
||||
end
|
||||
|
||||
def parse_url(url) do
|
||||
request = build_request_url(url)
|
||||
|
||||
with {:ok, %Tesla.Env{body: body}} <- HTTP.get(request) do
|
||||
Jason.decode(body)
|
||||
else
|
||||
error ->
|
||||
Logger.warn("""
|
||||
[NsfwApiPolicy]: The API server failed. Skipping.
|
||||
#{inspect(error)}
|
||||
""")
|
||||
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def check_url_nsfw(url) when is_binary(url) do
|
||||
threshold = Config.get([@policy, :threshold])
|
||||
|
||||
case parse_url(url) do
|
||||
{:ok, %{"score" => score}} when score >= threshold ->
|
||||
{:nsfw, %{url: url, score: score, threshold: threshold}}
|
||||
|
||||
{:ok, %{"score" => score}} ->
|
||||
{:sfw, %{url: url, score: score, threshold: threshold}}
|
||||
|
||||
_ ->
|
||||
{:sfw, %{url: url, score: nil, threshold: threshold}}
|
||||
end
|
||||
end
|
||||
|
||||
def check_url_nsfw(%{"href" => url}) when is_binary(url) do
|
||||
check_url_nsfw(url)
|
||||
end
|
||||
|
||||
def check_url_nsfw(url) do
|
||||
threshold = Config.get([@policy, :threshold])
|
||||
{:sfw, %{url: url, score: nil, threshold: threshold}}
|
||||
end
|
||||
|
||||
def check_attachment_nsfw(%{"url" => urls} = attachment) when is_list(urls) do
|
||||
if Enum.all?(urls, &match?({:sfw, _}, check_url_nsfw(&1))) do
|
||||
{:sfw, attachment}
|
||||
else
|
||||
{:nsfw, attachment}
|
||||
end
|
||||
end
|
||||
|
||||
def check_attachment_nsfw(%{"url" => url} = attachment) when is_binary(url) do
|
||||
case check_url_nsfw(url) do
|
||||
{:sfw, _} -> {:sfw, attachment}
|
||||
{:nsfw, _} -> {:nsfw, attachment}
|
||||
end
|
||||
end
|
||||
|
||||
def check_attachment_nsfw(attachment), do: {:sfw, attachment}
|
||||
|
||||
def check_object_nsfw(%{"attachment" => attachments} = object) when is_list(attachments) do
|
||||
if Enum.all?(attachments, &match?({:sfw, _}, check_attachment_nsfw(&1))) do
|
||||
{:sfw, object}
|
||||
else
|
||||
{:nsfw, object}
|
||||
end
|
||||
end
|
||||
|
||||
def check_object_nsfw(%{"object" => %{} = child_object} = object) do
|
||||
case check_object_nsfw(child_object) do
|
||||
{:sfw, _} -> {:sfw, object}
|
||||
{:nsfw, _} -> {:nsfw, object}
|
||||
end
|
||||
end
|
||||
|
||||
def check_object_nsfw(object), do: {:sfw, object}
|
||||
|
||||
@impl true
|
||||
def filter(object) do
|
||||
with {:sfw, object} <- check_object_nsfw(object) do
|
||||
{:ok, object}
|
||||
else
|
||||
{:nsfw, _data} -> handle_nsfw(object)
|
||||
_ -> {:reject, "NSFW: Attachment rejected"}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_nsfw(object) do
|
||||
if Config.get([@policy, :reject]) do
|
||||
{:reject, object}
|
||||
else
|
||||
{:ok,
|
||||
object
|
||||
|> maybe_unlist()
|
||||
|> maybe_mark_sensitive()}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_unlist(object) do
|
||||
if Config.get([@policy, :unlist]) do
|
||||
unlist(object)
|
||||
else
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_mark_sensitive(object) do
|
||||
if Config.get([@policy, :mark_sensitive]) do
|
||||
mark_sensitive(object)
|
||||
else
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
def unlist(%{"to" => to, "cc" => cc, "actor" => actor} = object) do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(actor) do
|
||||
to =
|
||||
[user.follower_address | to]
|
||||
|> List.delete(Constants.as_public())
|
||||
|> Enum.uniq()
|
||||
|
||||
cc =
|
||||
[Constants.as_public() | cc]
|
||||
|> List.delete(user.follower_address)
|
||||
|> Enum.uniq()
|
||||
|
||||
object
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
else
|
||||
_ -> raise "[NsfwApiPolicy]: Could not find user #{actor}"
|
||||
end
|
||||
end
|
||||
|
||||
def mark_sensitive(%{"object" => child_object} = object) when is_map(child_object) do
|
||||
Map.put(object, "object", mark_sensitive(child_object))
|
||||
end
|
||||
|
||||
def mark_sensitive(object) when is_map(object) do
|
||||
tags = (object["tag"] || []) ++ ["nsfw"]
|
||||
|
||||
object
|
||||
|> Map.put("tag", tags)
|
||||
|> Map.put("sensitive", true)
|
||||
end
|
||||
|
||||
# Hackney needs a trailing slash
|
||||
defp fix_path(%URI{path: path} = uri) when is_binary(path) do
|
||||
path = String.trim_trailing(path, "/") <> "/"
|
||||
Map.put(uri, :path, path)
|
||||
end
|
||||
|
||||
defp fix_path(%URI{path: nil} = uri), do: Map.put(uri, :path, "/")
|
||||
|
||||
@impl true
|
||||
def describe do
|
||||
options = %{
|
||||
threshold: Config.get([@policy, :threshold]),
|
||||
mark_sensitive: Config.get([@policy, :mark_sensitive]),
|
||||
unlist: Config.get([@policy, :unlist]),
|
||||
reject: Config.get([@policy, :reject])
|
||||
}
|
||||
|
||||
{:ok, %{@policy => options}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def config_description do
|
||||
%{
|
||||
key: @policy,
|
||||
related_policy: to_string(__MODULE__),
|
||||
label: "NSFW API Policy",
|
||||
description:
|
||||
"Hide, delete, or mark sensitive NSFW content with artificial intelligence. Requires running an external API server.",
|
||||
children: [
|
||||
%{
|
||||
key: :url,
|
||||
type: :string,
|
||||
description: "Base URL of the API server.",
|
||||
suggestions: ["http://127.0.0.1:5000/"]
|
||||
},
|
||||
%{
|
||||
key: :threshold,
|
||||
type: :float,
|
||||
description: "Lowest score to take action on. Between 0 and 1.",
|
||||
suggestions: [0.7]
|
||||
},
|
||||
%{
|
||||
key: :mark_sensitive,
|
||||
type: :boolean,
|
||||
description: "Mark sensitive all detected NSFW content?",
|
||||
suggestions: [true]
|
||||
},
|
||||
%{
|
||||
key: :unlist,
|
||||
type: :boolean,
|
||||
description: "Unlist sensitive all detected NSFW content?",
|
||||
suggestions: [false]
|
||||
},
|
||||
%{
|
||||
key: :reject,
|
||||
type: :boolean,
|
||||
description: "Reject sensitive all detected NSFW content (takes precedence)?",
|
||||
suggestions: [false]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.Policy do
|
||||
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
|
||||
@callback describe() :: {:ok | :error, Map.t()}
|
||||
@callback filter(map()) :: {:ok | :reject, map()}
|
||||
@callback describe() :: {:ok | :error, map()}
|
||||
@callback config_description() :: %{
|
||||
optional(:children) => [map()],
|
||||
key: atom(),
|
||||
|
|
|
|||
49
lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
Normal file
49
lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do
|
||||
@moduledoc "Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do
|
||||
{:ok, Map.put(activity, "object", filter_object(object))}
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(object), do: {:ok, object}
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def describe, do: {:ok, %{}}
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def history_awareness, do: :auto
|
||||
|
||||
defp filter_object(%{"quoteUrl" => quote_url} = object) do
|
||||
tags = object["tag"] || []
|
||||
|
||||
if Enum.any?(tags, fn tag ->
|
||||
CommonFixes.object_link_tag?(tag) and tag["href"] == quote_url
|
||||
end) do
|
||||
object
|
||||
else
|
||||
object
|
||||
|> Map.put(
|
||||
"tag",
|
||||
tags ++
|
||||
[
|
||||
%{
|
||||
"type" => "Link",
|
||||
"mediaType" => Pleroma.Constants.activity_json_canonical_mime_type(),
|
||||
"href" => quote_url
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -34,14 +34,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
|||
|> Path.basename()
|
||||
|> Path.extname()
|
||||
|
||||
file_path = Path.join(emoji_dir_path, shortcode <> (extension || ".png"))
|
||||
extension = if extension == "", do: ".png", else: extension
|
||||
|
||||
shortcode = Path.basename(shortcode)
|
||||
file_path = Path.join(emoji_dir_path, shortcode <> extension)
|
||||
|
||||
case File.write(file_path, response.body) do
|
||||
:ok ->
|
||||
shortcode
|
||||
|
||||
e ->
|
||||
Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
|
||||
Logger.warning("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
|
||||
nil
|
||||
end
|
||||
else
|
||||
|
|
@ -53,7 +56,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
|||
end
|
||||
else
|
||||
e ->
|
||||
Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
|
||||
Logger.warning("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
@ -76,6 +79,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
|||
new_emojis =
|
||||
foreign_emojis
|
||||
|> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end)
|
||||
|> Enum.reject(fn {shortcode, _url} -> String.contains?(shortcode, ["/", "\\"]) end)
|
||||
|> Enum.filter(fn {shortcode, _url} ->
|
||||
reject_emoji? =
|
||||
[:mrf_steal_emoji, :rejected_shortcodes]
|
||||
|
|
|
|||
15
lib/pleroma/web/activity_pub/mrf/utils.ex
Normal file
15
lib/pleroma/web/activity_pub/mrf/utils.ex
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.Utils do
|
||||
@spec describe_regex_or_string(String.t() | Regex.t()) :: String.t()
|
||||
def describe_regex_or_string(pattern) do
|
||||
# This horror is needed to convert regex sigils to strings
|
||||
if not is_binary(pattern) do
|
||||
inspect(pattern)
|
||||
else
|
||||
pattern
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -21,7 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
|
||||
|
|
@ -102,7 +102,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
%{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
|
||||
meta
|
||||
)
|
||||
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
|
||||
when objtype in ~w[Question Answer Audio Video Image Event Article Note Page] do
|
||||
with {:ok, object_data} <- cast_and_apply_and_stringify_with_history(object),
|
||||
meta = Keyword.put(meta, :object_data, object_data),
|
||||
{:ok, create_activity} <-
|
||||
|
|
@ -115,13 +115,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
end
|
||||
|
||||
def validate(%{"type" => type} = object, meta)
|
||||
when type in ~w[Event Question Audio Video Article Note Page] do
|
||||
when type in ~w[Event Question Audio Video Image Article Note Page] do
|
||||
validator =
|
||||
case type do
|
||||
"Event" -> EventValidator
|
||||
"Question" -> QuestionValidator
|
||||
"Audio" -> AudioVideoValidator
|
||||
"Video" -> AudioVideoValidator
|
||||
"Audio" -> AudioImageVideoValidator
|
||||
"Video" -> AudioImageVideoValidator
|
||||
"Image" -> AudioImageVideoValidator
|
||||
"Article" -> ArticleNotePageValidator
|
||||
"Note" -> ArticleNotePageValidator
|
||||
"Page" -> ArticleNotePageValidator
|
||||
|
|
@ -172,6 +173,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
|
||||
{:object_validation, e} ->
|
||||
e
|
||||
|
||||
{:error, %Ecto.Changeset{} = e} ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -233,8 +237,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
AnswerValidator.cast_and_apply(object)
|
||||
end
|
||||
|
||||
def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Video] do
|
||||
AudioVideoValidator.cast_and_apply(object)
|
||||
def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Image Video] do
|
||||
AudioImageVideoValidator.cast_and_apply(object)
|
||||
end
|
||||
|
||||
def cast_and_apply(%{"type" => "Event"} = object) do
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator do
|
|||
end
|
||||
|
||||
defp maybe_refetch_user(%User{ap_id: ap_id}) do
|
||||
Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
||||
# Maybe it could use User.get_or_fetch_by_ap_id to avoid refreshing too often
|
||||
User.fetch_by_ap_id(ap_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
|
|||
object when is_binary(object) <- get_field(cng, :object),
|
||||
%User{} = actor <- User.get_cached_by_ap_id(actor),
|
||||
%Object{} = object <- Object.get_cached_by_ap_id(object),
|
||||
false <- Visibility.is_public?(object) do
|
||||
false <- Visibility.public?(object) do
|
||||
same_actor = object.data["actor"] == actor.ap_id
|
||||
recipients = get_field(cng, :to) ++ get_field(cng, :cc)
|
||||
local_public = Utils.as_local_public()
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
|> fix_tag()
|
||||
|> fix_replies()
|
||||
|> fix_attachments()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> Transmogrifier.fix_content_map()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|
|||
@primary_key false
|
||||
embedded_schema do
|
||||
field(:id, :string)
|
||||
field(:type, :string)
|
||||
field(:type, :string, default: "Link")
|
||||
field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
|
||||
field(:name, :string)
|
||||
field(:summary, :string)
|
||||
field(:blurhash, :string)
|
||||
|
||||
embeds_many :url, UrlObjectValidator, primary_key: false do
|
||||
field(:type, :string)
|
||||
field(:type, :string, default: "Link")
|
||||
field(:href, ObjectValidators.Uri)
|
||||
field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
|
||||
field(:width, :integer)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
|
|
@ -55,9 +55,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|
|||
url
|
||||
|> Enum.concat(mpeg_url["tag"] || [])
|
||||
|> Enum.find(fn
|
||||
%{"mediaType" => mime_type} -> String.starts_with?(mime_type, ["video/", "audio/"])
|
||||
%{"mimeType" => mime_type} -> String.starts_with?(mime_type, ["video/", "audio/"])
|
||||
_ -> false
|
||||
%{"mediaType" => mime_type} ->
|
||||
String.starts_with?(mime_type, ["video/", "audio/", "image/"])
|
||||
|
||||
%{"mimeType" => mime_type} ->
|
||||
String.starts_with?(mime_type, ["video/", "audio/", "image/"])
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -94,6 +99,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|
|||
data
|
||||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_object_defaults()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> fix_url()
|
||||
|> fix_content()
|
||||
|
|
@ -110,7 +116,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|
|||
|
||||
defp validate_data(data_cng) do
|
||||
data_cng
|
||||
|> validate_inclusion(:type, ["Audio", "Video"])
|
||||
|> validate_inclusion(:type, ~w[Audio Image Video])
|
||||
|> validate_required([:id, :actor, :attributedTo, :type, :context])
|
||||
|> CommonValidations.validate_any_presence([:cc, :to])
|
||||
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|
||||
|
|
@ -57,6 +57,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
|
|||
|> Map.put("attachment", attachment)
|
||||
end
|
||||
|
||||
def fix_attachment(%{"attachment" => attachment} = data) when attachment == [] do
|
||||
data
|
||||
|> Map.drop(["attachment"])
|
||||
end
|
||||
|
||||
def fix_attachment(data), do: data
|
||||
|
||||
def changeset(struct, data) do
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
|
|||
end
|
||||
end
|
||||
|
||||
# All objects except Answer and CHatMessage
|
||||
# All objects except Answer and ChatMessage
|
||||
defmacro object_fields do
|
||||
quote bind_quoted: binding() do
|
||||
field(:content, :string)
|
||||
|
|
@ -57,8 +57,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
|
|||
field(:replies_count, :integer, default: 0)
|
||||
field(:like_count, :integer, default: 0)
|
||||
field(:announcement_count, :integer, default: 0)
|
||||
field(:quotes_count, :integer, default: 0)
|
||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||
field(:url, ObjectValidators.Uri)
|
||||
field(:quoteUrl, ObjectValidators.ObjectID)
|
||||
field(:url, ObjectValidators.BareUri)
|
||||
|
||||
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
field(:announcements, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
|
||||
{:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback)
|
||||
|
||||
|
|
@ -22,6 +25,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
end
|
||||
|
||||
def fix_object_defaults(data) do
|
||||
data = Maps.filter_empty_values(data)
|
||||
|
||||
context =
|
||||
Utils.maybe_create_context(
|
||||
data["context"] || data["conversation"] || data["inReplyTo"] || data["id"]
|
||||
|
|
@ -76,4 +81,48 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
|
||||
Map.put(data, "to", to)
|
||||
end
|
||||
|
||||
def fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data
|
||||
|
||||
# Fedibird
|
||||
# https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
|
||||
def fix_quote_url(%{"quoteUri" => quote_url} = data) do
|
||||
Map.put(data, "quoteUrl", quote_url)
|
||||
end
|
||||
|
||||
# Old Fedibird (bug)
|
||||
# https://github.com/fedibird/mastodon/issues/9
|
||||
def fix_quote_url(%{"quoteURL" => quote_url} = data) do
|
||||
Map.put(data, "quoteUrl", quote_url)
|
||||
end
|
||||
|
||||
# Misskey fallback
|
||||
def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do
|
||||
Map.put(data, "quoteUrl", quote_url)
|
||||
end
|
||||
|
||||
def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do
|
||||
tag = Enum.find(tags, &object_link_tag?/1)
|
||||
|
||||
if not is_nil(tag) do
|
||||
data
|
||||
|> Map.put("quoteUrl", tag["href"])
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
def fix_quote_url(data), do: data
|
||||
|
||||
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
|
||||
def object_link_tag?(%{
|
||||
"type" => "Link",
|
||||
"mediaType" => media_type,
|
||||
"href" => href
|
||||
})
|
||||
when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do
|
||||
true
|
||||
end
|
||||
|
||||
def object_link_tag?(_), do: false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
|
@ -19,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields
|
||||
message_fields()
|
||||
activity_fields()
|
||||
embeds_many(:tag, TagValidator)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -43,7 +46,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
|
||||
def changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, __schema__(:fields))
|
||||
|> cast(data, __schema__(:fields) -- [:tag])
|
||||
|> cast_embed(:tag)
|
||||
end
|
||||
|
||||
defp fix(data) do
|
||||
|
|
@ -53,12 +57,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_activity_addressing()
|
||||
|
||||
with %Object{} = object <- Object.normalize(data["object"]) do
|
||||
data
|
||||
|> CommonFixes.fix_activity_context(object)
|
||||
|> CommonFixes.fix_object_action_recipients(object)
|
||||
else
|
||||
_ -> data
|
||||
data = Map.put_new(data, "tag", [])
|
||||
|
||||
case Object.normalize(data["object"]) do
|
||||
%Object{} = object ->
|
||||
data
|
||||
|> CommonFixes.fix_activity_context(object)
|
||||
|> CommonFixes.fix_object_action_recipients(object)
|
||||
|
||||
_ ->
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -66,10 +74,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji)
|
||||
|
||||
cond do
|
||||
Pleroma.Emoji.is_unicode_emoji?(emoji) ->
|
||||
Pleroma.Emoji.unicode?(emoji) ->
|
||||
data
|
||||
|
||||
Pleroma.Emoji.is_unicode_emoji?(new_emoji) ->
|
||||
Pleroma.Emoji.unicode?(new_emoji) ->
|
||||
data |> Map.put("content", new_emoji)
|
||||
|
||||
true ->
|
||||
|
|
@ -82,11 +90,31 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
defp validate_emoji(cng) do
|
||||
content = get_field(cng, :content)
|
||||
|
||||
if Pleroma.Emoji.is_unicode_emoji?(content) do
|
||||
if Emoji.unicode?(content) || Emoji.custom?(content) do
|
||||
cng
|
||||
else
|
||||
cng
|
||||
|> add_error(:content, "must be a single character emoji")
|
||||
|> add_error(:content, "is not a valid emoji")
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_validate_tag_presence(cng) do
|
||||
content = get_field(cng, :content)
|
||||
|
||||
if Emoji.unicode?(content) do
|
||||
cng
|
||||
else
|
||||
tag = get_field(cng, :tag)
|
||||
emoji_name = Emoji.maybe_strip_name(content)
|
||||
|
||||
case tag do
|
||||
[%{name: ^emoji_name, type: "Emoji", icon: %{url: _}}] ->
|
||||
cng
|
||||
|
||||
_ ->
|
||||
cng
|
||||
|> add_error(:tag, "does not contain an Emoji tag")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -97,5 +125,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
|> validate_actor_presence()
|
||||
|> validate_object_presence()
|
||||
|> validate_emoji()
|
||||
|> maybe_validate_tag_presence()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator do
|
|||
|
||||
embeds_one :replies, Replies, primary_key: false do
|
||||
field(:totalItems, :integer)
|
||||
field(:type, :string)
|
||||
field(:type, :string, default: "Collection")
|
||||
end
|
||||
|
||||
field(:type, :string)
|
||||
field(:type, :string, default: "Note")
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|
|||
|
||||
field(:closed, ObjectValidators.DateTime)
|
||||
field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
field(:nonAnonymous, :boolean)
|
||||
embeds_many(:anyOf, QuestionOptionsValidator)
|
||||
embeds_many(:oneOf, QuestionOptionsValidator)
|
||||
end
|
||||
|
|
@ -62,6 +63,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|
|||
data
|
||||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_object_defaults()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> fix_closed()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,15 +9,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
|
|||
|
||||
import Ecto.Changeset
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
# Common
|
||||
field(:type, :string)
|
||||
field(:name, :string)
|
||||
|
||||
# Mention, Hashtag
|
||||
# Mention, Hashtag, Link
|
||||
field(:href, ObjectValidators.Uri)
|
||||
|
||||
# Link
|
||||
field(:mediaType, :string)
|
||||
|
||||
# Emoji
|
||||
embeds_one :icon, IconObjectValidator, primary_key: false do
|
||||
field(:type, :string)
|
||||
|
|
@ -68,6 +73,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
|
|||
|> validate_required([:type, :name, :icon])
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => "Link"} = data) do
|
||||
struct
|
||||
|> cast(data, [:type, :name, :mediaType, :href])
|
||||
|> validate_inclusion(:mediaType, Pleroma.Constants.activity_json_mime_types())
|
||||
|> validate_required([:type, :href, :mediaType])
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => _} = data) do
|
||||
struct
|
||||
|> cast(data, [])
|
||||
|> Map.put(:action, :ignore)
|
||||
end
|
||||
|
||||
def icon_changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, [:type, :url])
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
|
|||
with {:ok, local} <- Keyword.fetch(meta, :local) do
|
||||
do_not_federate = meta[:do_not_federate] || !config().get([:instance, :federating])
|
||||
|
||||
if !do_not_federate and local and not Visibility.is_local_public?(activity) do
|
||||
if !do_not_federate and local and not Visibility.local_public?(activity) do
|
||||
activity =
|
||||
if object = Keyword.get(meta, :object_data) do
|
||||
%{activity | data: Map.put(activity.data, "object", object)}
|
||||
|
|
|
|||
|
|
@ -13,23 +13,60 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Workers.PublisherWorker
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
import Pleroma.Web.ActivityPub.Visibility
|
||||
|
||||
@behaviour Pleroma.Web.Federator.Publisher
|
||||
|
||||
require Logger
|
||||
|
||||
@moduledoc """
|
||||
ActivityPub outgoing federation module.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Enqueue publishing a single activity.
|
||||
"""
|
||||
@spec enqueue_one(map(), Keyword.t()) :: {:ok, %Oban.Job{}}
|
||||
def enqueue_one(%{} = params, worker_args \\ []) do
|
||||
PublisherWorker.enqueue(
|
||||
"publish_one",
|
||||
%{"params" => params},
|
||||
worker_args
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gathers a set of remote users given an IR envelope.
|
||||
"""
|
||||
def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
|
||||
cc = Map.get(data, "cc", [])
|
||||
|
||||
bcc =
|
||||
data
|
||||
|> Map.get("bcc", [])
|
||||
|> Enum.reduce([], fn ap_id, bcc ->
|
||||
case Pleroma.List.get_by_ap_id(ap_id) do
|
||||
%Pleroma.List{user_id: ^user_id} = list ->
|
||||
{:ok, following} = Pleroma.List.get_following(list)
|
||||
bcc ++ Enum.map(following, & &1.ap_id)
|
||||
|
||||
_ ->
|
||||
bcc
|
||||
end
|
||||
end)
|
||||
|
||||
[to, cc, bcc]
|
||||
|> Enum.concat()
|
||||
|> Enum.map(&User.get_cached_by_ap_id/1)
|
||||
|> Enum.filter(fn user -> user && !user.local end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determine if an activity can be represented by running it through Transmogrifier.
|
||||
"""
|
||||
def is_representable?(%Activity{} = activity) do
|
||||
def representable?(%Activity{} = activity) do
|
||||
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
|
||||
true
|
||||
else
|
||||
|
|
@ -80,9 +117,27 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
|
||||
result
|
||||
else
|
||||
{_post_result, response} ->
|
||||
{_post_result, %{status: code} = response} = e ->
|
||||
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
|
||||
{:error, response}
|
||||
Logger.metadata(activity: id, inbox: inbox, status: code)
|
||||
Logger.error("Publisher failed to inbox #{inbox} with status #{code}")
|
||||
|
||||
case response do
|
||||
%{status: 403} -> {:discard, :forbidden}
|
||||
%{status: 404} -> {:discard, :not_found}
|
||||
%{status: 410} -> {:discard, :not_found}
|
||||
_ -> {:error, e}
|
||||
end
|
||||
|
||||
{:error, :pool_full} ->
|
||||
Logger.debug("Publisher snoozing worker job due to full connection pool")
|
||||
{:snooze, 30}
|
||||
|
||||
e ->
|
||||
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
|
||||
Logger.metadata(activity: id, inbox: inbox)
|
||||
Logger.error("Publisher failed to inbox #{inbox} #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -103,22 +158,21 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
end
|
||||
end
|
||||
|
||||
defp should_federate?(inbox, public) do
|
||||
if public do
|
||||
true
|
||||
else
|
||||
%{host: host} = URI.parse(inbox)
|
||||
def should_federate?(nil, _), do: false
|
||||
def should_federate?(_, true), do: true
|
||||
|
||||
quarantined_instances =
|
||||
Config.get([:instance, :quarantined_instances], [])
|
||||
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|
||||
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
|
||||
def should_federate?(inbox, _) do
|
||||
%{host: host} = URI.parse(inbox)
|
||||
|
||||
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
|
||||
end
|
||||
quarantined_instances =
|
||||
Config.get([:instance, :quarantined_instances], [])
|
||||
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|
||||
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
|
||||
|
||||
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
|
||||
end
|
||||
|
||||
@spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
|
||||
@spec recipients(User.t(), Activity.t()) :: [[User.t()]]
|
||||
defp recipients(actor, activity) do
|
||||
followers =
|
||||
if actor.follower_address in activity.recipients do
|
||||
|
|
@ -138,7 +192,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
[]
|
||||
end
|
||||
|
||||
Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
|
||||
mentioned = remote_users(actor, activity)
|
||||
non_mentioned = (followers ++ fetchers) -- mentioned
|
||||
|
||||
[mentioned, non_mentioned]
|
||||
end
|
||||
|
||||
defp get_cc_ap_ids(ap_id, recipients) do
|
||||
|
|
@ -192,45 +249,52 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
|
||||
def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
|
||||
when is_list(bcc) and bcc != [] do
|
||||
public = is_public?(activity)
|
||||
public = public?(activity)
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
|
||||
recipients = recipients(actor, activity)
|
||||
[priority_recipients, recipients] = recipients(actor, activity)
|
||||
|
||||
inboxes =
|
||||
recipients
|
||||
|> Enum.filter(&User.ap_enabled?/1)
|
||||
|> Enum.map(fn actor -> actor.inbox end)
|
||||
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|
||||
|> Instances.filter_reachable()
|
||||
[priority_recipients, recipients]
|
||||
|> Enum.map(fn recipients ->
|
||||
recipients
|
||||
|> Enum.map(fn %User{} = user ->
|
||||
determine_inbox(activity, user)
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|
||||
|> Instances.filter_reachable()
|
||||
end)
|
||||
|
||||
Repo.checkout(fn ->
|
||||
Enum.each(inboxes, fn {inbox, unreachable_since} ->
|
||||
%User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
|
||||
Enum.each(inboxes, fn inboxes ->
|
||||
Enum.each(inboxes, fn {inbox, unreachable_since} ->
|
||||
%User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
|
||||
|
||||
# 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)
|
||||
# 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)
|
||||
|
||||
json =
|
||||
data
|
||||
|> Map.put("cc", cc)
|
||||
|> Jason.encode!()
|
||||
json =
|
||||
data
|
||||
|> Map.put("cc", cc)
|
||||
|> Jason.encode!()
|
||||
|
||||
Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor_id: actor.id,
|
||||
id: activity.data["id"],
|
||||
unreachable_since: unreachable_since
|
||||
})
|
||||
__MODULE__.enqueue_one(%{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor_id: actor.id,
|
||||
id: activity.data["id"],
|
||||
unreachable_since: unreachable_since
|
||||
})
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
# Publishes an activity to all relevant peers.
|
||||
def publish(%User{} = actor, %Activity{} = activity) do
|
||||
public = is_public?(activity)
|
||||
public = public?(activity)
|
||||
|
||||
if public && Config.get([:instance, :allow_relay]) do
|
||||
Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
|
||||
|
|
@ -240,26 +304,38 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
json = Jason.encode!(data)
|
||||
|
||||
recipients(actor, activity)
|
||||
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|
||||
|> Enum.map(fn %User{} = user ->
|
||||
determine_inbox(activity, user)
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|
||||
|> Instances.filter_reachable()
|
||||
|> Enum.each(fn {inbox, unreachable_since} ->
|
||||
Pleroma.Web.Federator.Publisher.enqueue_one(
|
||||
__MODULE__,
|
||||
%{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor_id: actor.id,
|
||||
id: activity.data["id"],
|
||||
unreachable_since: unreachable_since
|
||||
}
|
||||
)
|
||||
[priority_inboxes, inboxes] =
|
||||
recipients(actor, activity)
|
||||
|> Enum.map(fn recipients ->
|
||||
recipients
|
||||
|> Enum.map(fn %User{} = user ->
|
||||
determine_inbox(activity, user)
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|
||||
end)
|
||||
|
||||
inboxes = inboxes -- priority_inboxes
|
||||
|
||||
[{priority_inboxes, 0}, {inboxes, 1}]
|
||||
|> Enum.each(fn {inboxes, priority} ->
|
||||
inboxes
|
||||
|> Instances.filter_reachable()
|
||||
|> Enum.each(fn {inbox, unreachable_since} ->
|
||||
__MODULE__.enqueue_one(
|
||||
%{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor_id: actor.id,
|
||||
id: activity.data["id"],
|
||||
unreachable_since: unreachable_since
|
||||
},
|
||||
priority: priority
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def gather_webfinger_links(%User{} = user) do
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
|
|||
@spec publish(any()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
|
||||
with %User{} = user <- get_actor(),
|
||||
true <- Visibility.is_public?(activity) do
|
||||
true <- Visibility.public?(activity) do
|
||||
CommonAPI.repeat(activity.id, user)
|
||||
else
|
||||
error -> format_error(error)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Push
|
||||
alias Pleroma.Web.Streamer
|
||||
alias Pleroma.Workers.PollWorker
|
||||
|
||||
|
|
@ -125,7 +124,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
nil
|
||||
end
|
||||
|
||||
{:ok, notifications} = Notification.create_notifications(object, do_send: false)
|
||||
{:ok, notifications} = Notification.create_notifications(object)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|
|
@ -184,7 +183,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
liked_object = Object.get_by_ap_id(object.data["object"])
|
||||
Utils.add_like_to_object(object, liked_object)
|
||||
|
||||
Notification.create_notifications(object)
|
||||
{:ok, notifications} = Notification.create_notifications(object)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_notifications(notifications)
|
||||
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
|
@ -197,11 +200,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
# - Increase replies count
|
||||
# - Set up ActivityExpiration
|
||||
# - Set up notifications
|
||||
# - Index incoming posts for search (if needed)
|
||||
@impl true
|
||||
def handle(%{data: %{"type" => "Create"}} = activity, meta) do
|
||||
with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta),
|
||||
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
|
||||
{:ok, notifications} = Notification.create_notifications(activity, do_send: false)
|
||||
{:ok, notifications} = Notification.create_notifications(activity)
|
||||
{:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
|
||||
{:ok, _user} = ActivityPub.update_last_status_at_if_public(user, object)
|
||||
|
||||
|
|
@ -209,6 +213,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
Object.increase_replies_count(in_reply_to)
|
||||
end
|
||||
|
||||
if quote_url = object.data["quoteUrl"] do
|
||||
Object.increase_quotes_count(quote_url)
|
||||
end
|
||||
|
||||
reply_depth = (meta[:depth] || 0) + 1
|
||||
|
||||
# FIXME: Force inReplyTo to replies
|
||||
|
|
@ -222,9 +230,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
end
|
||||
end
|
||||
|
||||
ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
|
||||
Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
|
||||
end)
|
||||
Pleroma.Web.RichMedia.Card.get_by_activity(activity)
|
||||
|
||||
Pleroma.Search.add_to_index(Map.put(activity, :object, object))
|
||||
|
||||
Utils.maybe_handle_group_posts(activity)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|
|
@ -249,11 +259,13 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
|
||||
Utils.add_announce_to_object(object, announced_object)
|
||||
|
||||
if !User.is_internal_user?(user) do
|
||||
Notification.create_notifications(object)
|
||||
{:ok, notifications} = Notification.create_notifications(object)
|
||||
|
||||
ap_streamer().stream_out(object)
|
||||
end
|
||||
if !User.internal?(user), do: ap_streamer().stream_out(object)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_notifications(notifications)
|
||||
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
|
@ -274,7 +286,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
reacted_object = Object.get_by_ap_id(object.data["object"])
|
||||
Utils.add_emoji_reaction_to_object(object, reacted_object)
|
||||
|
||||
Notification.create_notifications(object)
|
||||
{:ok, notifications} = Notification.create_notifications(object)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_notifications(notifications)
|
||||
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
|
@ -285,6 +301,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
# - Reduce the user note count
|
||||
# - Reduce the reply count
|
||||
# - Stream out the activity
|
||||
# - Removes posts from search index (if needed)
|
||||
@impl true
|
||||
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
|
||||
deleted_object =
|
||||
|
|
@ -294,9 +311,9 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
result =
|
||||
case deleted_object do
|
||||
%Object{} ->
|
||||
with {:ok, deleted_object, _activity} <- Object.delete(deleted_object),
|
||||
with {_, {:ok, deleted_object, _activity}} <- {:object, Object.delete(deleted_object)},
|
||||
{_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]},
|
||||
%User{} = user <- User.get_cached_by_ap_id(actor) do
|
||||
{_, %User{} = user} <- {:user, User.get_cached_by_ap_id(actor)} do
|
||||
User.remove_pinned_object_id(user, deleted_object.data["id"])
|
||||
|
||||
{:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
|
||||
|
|
@ -305,6 +322,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
Object.decrease_replies_count(in_reply_to)
|
||||
end
|
||||
|
||||
if quote_url = deleted_object.data["quoteUrl"] do
|
||||
Object.decrease_quotes_count(quote_url)
|
||||
end
|
||||
|
||||
MessageReference.delete_for_object(deleted_object)
|
||||
|
||||
ap_streamer().stream_out(object)
|
||||
|
|
@ -314,6 +335,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
{:actor, _} ->
|
||||
@logger.error("The object doesn't have an actor: #{inspect(deleted_object)}")
|
||||
:no_object_actor
|
||||
|
||||
{:user, _} ->
|
||||
@logger.error(
|
||||
"The object's actor could not be resolved to a user: #{inspect(deleted_object)}"
|
||||
)
|
||||
|
||||
:no_object_user
|
||||
|
||||
{:object, _} ->
|
||||
@logger.error("The object could not be deleted: #{inspect(deleted_object)}")
|
||||
{:error, object}
|
||||
end
|
||||
|
||||
%User{} ->
|
||||
|
|
@ -323,6 +355,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
end
|
||||
|
||||
if result == :ok do
|
||||
# Only remove from index when deleting actual objects, not users or anything else
|
||||
with %Pleroma.Object{} <- deleted_object do
|
||||
Pleroma.Search.remove_from_index(deleted_object)
|
||||
end
|
||||
|
||||
{:ok, object, meta}
|
||||
else
|
||||
{:error, result}
|
||||
|
|
@ -428,37 +465,13 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
end
|
||||
|
||||
if orig_object_data["type"] in Pleroma.Constants.updatable_object_types() do
|
||||
%{
|
||||
updated_data: updated_object_data,
|
||||
updated: updated,
|
||||
used_history_in_new_object?: used_history_in_new_object?
|
||||
} = Object.Updater.make_new_object_data_from_update_object(orig_object_data, updated_object)
|
||||
{:ok, _, updated} =
|
||||
Object.Updater.do_update_and_invalidate_cache(orig_object, updated_object)
|
||||
|
||||
changeset =
|
||||
orig_object
|
||||
|> Repo.preload(:hashtags)
|
||||
|> Object.change(%{data: updated_object_data})
|
||||
|
||||
with {:ok, new_object} <- Repo.update(changeset),
|
||||
{:ok, _} <- Object.invalid_object_cache(new_object),
|
||||
{:ok, _} <- Object.set_cache(new_object),
|
||||
# The metadata/utils.ex uses the object id for the cache.
|
||||
{:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(new_object.id) do
|
||||
if used_history_in_new_object? do
|
||||
with create_activity when not is_nil(create_activity) <-
|
||||
Pleroma.Activity.get_create_by_object_ap_id(orig_object_ap_id),
|
||||
{:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(create_activity.id) do
|
||||
nil
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
if updated do
|
||||
object
|
||||
|> Activity.normalize()
|
||||
|> ActivityPub.notify_and_stream()
|
||||
end
|
||||
if updated do
|
||||
object
|
||||
|> Activity.normalize()
|
||||
|> ActivityPub.notify_and_stream()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -520,7 +533,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
end
|
||||
|
||||
def handle_object_creation(%{"type" => objtype} = object, _activity, meta)
|
||||
when objtype in ~w[Audio Video Event Article Note Page] do
|
||||
when objtype in ~w[Audio Video Image Event Article Note Page] do
|
||||
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
|
@ -574,17 +587,14 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
|
||||
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
|
||||
|
||||
@spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()}
|
||||
@spec delete_object(Activity.t()) :: :ok | {:error, Ecto.Changeset.t()}
|
||||
defp delete_object(object) do
|
||||
with {:ok, _} <- Repo.delete(object), do: :ok
|
||||
end
|
||||
|
||||
defp send_notifications(meta) do
|
||||
Keyword.get(meta, :notifications, [])
|
||||
|> Enum.each(fn notification ->
|
||||
Streamer.stream(["user", "user:notification"], notification)
|
||||
Push.send(notification)
|
||||
end)
|
||||
|> Notification.send()
|
||||
|
||||
meta
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@
|
|||
|
||||
defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do
|
||||
@callback handle(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
|
||||
@callback handle_after_transaction(map()) :: map()
|
||||
@callback handle_after_transaction(keyword()) :: keyword()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,11 +20,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Workers.TransmogrifierWorker
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
@doc """
|
||||
|
|
@ -156,8 +154,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.put("context", replied_object.data["context"] || object["conversation"])
|
||||
|> Map.drop(["conversation", "inReplyToAtomUri"])
|
||||
else
|
||||
e ->
|
||||
Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
||||
_ ->
|
||||
object
|
||||
end
|
||||
else
|
||||
|
|
@ -167,6 +164,26 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def fix_in_reply_to(object, _options), do: object
|
||||
|
||||
def fix_quote_url_and_maybe_fetch(object, options \\ []) do
|
||||
quote_url =
|
||||
case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do
|
||||
%{"quoteUrl" => quote_url} -> quote_url
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
with {:quoting?, true} <- {:quoting?, not is_nil(quote_url)},
|
||||
{:ok, quoted_object} <- get_obj_helper(quote_url, options),
|
||||
%Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do
|
||||
Map.put(object, "quoteUrl", quoted_object.data["id"])
|
||||
else
|
||||
{:quoting?, _} ->
|
||||
object
|
||||
|
||||
_ ->
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_in_reply_to(in_reply_to) do
|
||||
cond do
|
||||
is_bitstring(in_reply_to) ->
|
||||
|
|
@ -447,7 +464,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
|
||||
options
|
||||
)
|
||||
when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page} do
|
||||
when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do
|
||||
fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
|
||||
|
||||
object =
|
||||
|
|
@ -455,6 +472,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> strip_internal_fields()
|
||||
|> fix_type(fetch_options)
|
||||
|> fix_in_reply_to(fetch_options)
|
||||
|> fix_quote_url_and_maybe_fetch(fetch_options)
|
||||
|
||||
data = Map.put(data, "object", object)
|
||||
options = Keyword.put(options, :local, false)
|
||||
|
|
@ -629,6 +647,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def set_reply_to_uri(obj), do: obj
|
||||
|
||||
@doc """
|
||||
Fedibird compatibility
|
||||
https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
|
||||
"""
|
||||
def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do
|
||||
Map.put(object, "quoteUri", quote_url)
|
||||
end
|
||||
|
||||
def set_quote_url(obj), do: obj
|
||||
|
||||
@doc """
|
||||
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
|
||||
Based on Mastodon's ActivityPub::NoteSerializer#replies.
|
||||
|
|
@ -683,6 +711,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> prepare_attachments
|
||||
|> set_conversation
|
||||
|> set_reply_to_uri
|
||||
|> set_quote_url
|
||||
|> set_replies
|
||||
|> strip_internal_fields
|
||||
|> strip_internal_tags
|
||||
|
|
@ -750,7 +779,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Object.normalize(fetch: false)
|
||||
|
||||
data =
|
||||
if Visibility.is_private?(object) && object.data["actor"] == ap_id do
|
||||
if Visibility.private?(object) && object.data["actor"] == ap_id do
|
||||
data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
|
||||
else
|
||||
data |> maybe_fix_object_url
|
||||
|
|
@ -820,8 +849,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
relative_object do
|
||||
Map.put(data, "object", external_url)
|
||||
else
|
||||
{:fetch, e} ->
|
||||
Logger.error("Couldn't fetch #{object} #{inspect(e)}")
|
||||
{:fetch, _} ->
|
||||
data
|
||||
|
||||
_ ->
|
||||
|
|
@ -946,47 +974,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
defp strip_internal_tags(object), do: object
|
||||
|
||||
def perform(:user_upgrade, user) do
|
||||
# we pass a fake user so that the followers collection is stripped away
|
||||
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
|
||||
|
||||
from(
|
||||
a in Activity,
|
||||
where: ^old_follower_address in a.recipients,
|
||||
update: [
|
||||
set: [
|
||||
recipients:
|
||||
fragment(
|
||||
"array_replace(?,?,?)",
|
||||
a.recipients,
|
||||
^old_follower_address,
|
||||
^user.follower_address
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|> Repo.update_all([])
|
||||
end
|
||||
|
||||
def upgrade_user_from_ap_id(ap_id) do
|
||||
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
|
||||
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
|
||||
{:ok, user} <- update_user(user, data) do
|
||||
{:ok, _pid} = Task.start(fn -> ActivityPub.pinned_fetch_task(user) end)
|
||||
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
|
||||
{:ok, user}
|
||||
else
|
||||
%User{} = user -> {:ok, user}
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
defp update_user(user, data) do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
|
||||
Map.put(data, "url", url["href"])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
alias Ecto.UUID
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
|
|
@ -31,7 +32,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
"Page",
|
||||
"Question",
|
||||
"Answer",
|
||||
"Audio"
|
||||
"Audio",
|
||||
"Image"
|
||||
]
|
||||
@strip_status_report_states ~w(closed resolved)
|
||||
@supported_report_states ~w(open closed resolved)
|
||||
|
|
@ -165,7 +167,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
|
||||
with true <- Config.get!([:instance, :federating]),
|
||||
true <- type != "Block" || outgoing_blocks,
|
||||
false <- Visibility.is_local_public?(activity) do
|
||||
false <- Visibility.local_public?(activity) do
|
||||
Pleroma.Web.Federator.publish(activity)
|
||||
end
|
||||
|
||||
|
|
@ -275,7 +277,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
object_actor = User.get_cached_by_ap_id(object_actor_id)
|
||||
|
||||
to =
|
||||
if Visibility.is_public?(object) do
|
||||
if Visibility.public?(object) do
|
||||
[actor.follower_address, object.data["actor"]]
|
||||
else
|
||||
[object.data["actor"]]
|
||||
|
|
@ -325,21 +327,29 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
||||
|
||||
def add_emoji_reaction_to_object(
|
||||
%Activity{data: %{"content" => emoji, "actor" => actor}},
|
||||
%Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
|
||||
object
|
||||
) do
|
||||
reactions = get_cached_emoji_reactions(object)
|
||||
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
|
||||
url = maybe_emoji_url(emoji, activity)
|
||||
|
||||
new_reactions =
|
||||
case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
|
||||
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
|
||||
if is_nil(candidate_url) do
|
||||
emoji == candidate
|
||||
else
|
||||
url == candidate_url
|
||||
end
|
||||
end) do
|
||||
nil ->
|
||||
reactions ++ [[emoji, [actor]]]
|
||||
reactions ++ [[emoji, [actor], url]]
|
||||
|
||||
index ->
|
||||
List.update_at(
|
||||
reactions,
|
||||
index,
|
||||
fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end
|
||||
fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -348,18 +358,40 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
update_element_in_object("reaction", new_reactions, object, count)
|
||||
end
|
||||
|
||||
defp maybe_emoji_url(
|
||||
name,
|
||||
%Activity{
|
||||
data: %{
|
||||
"tag" => [
|
||||
%{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
|
||||
]
|
||||
}
|
||||
}
|
||||
),
|
||||
do: url
|
||||
|
||||
defp maybe_emoji_url(_, _), do: nil
|
||||
|
||||
def emoji_count(reactions_list) do
|
||||
Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end)
|
||||
Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)
|
||||
end
|
||||
|
||||
def remove_emoji_reaction_from_object(
|
||||
%Activity{data: %{"content" => emoji, "actor" => actor}},
|
||||
%Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
|
||||
object
|
||||
) do
|
||||
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
|
||||
reactions = get_cached_emoji_reactions(object)
|
||||
url = maybe_emoji_url(emoji, activity)
|
||||
|
||||
new_reactions =
|
||||
case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
|
||||
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
|
||||
if is_nil(candidate_url) do
|
||||
emoji == candidate
|
||||
else
|
||||
url == candidate_url
|
||||
end
|
||||
end) do
|
||||
nil ->
|
||||
reactions
|
||||
|
||||
|
|
@ -367,9 +399,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
List.update_at(
|
||||
reactions,
|
||||
index,
|
||||
fn [emoji, users] -> [emoji, List.delete(users, actor)] end
|
||||
fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
|
||||
)
|
||||
|> Enum.reject(fn [_, users] -> Enum.empty?(users) end)
|
||||
|> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
|
||||
end
|
||||
|
||||
count = emoji_count(new_reactions)
|
||||
|
|
@ -377,11 +409,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
end
|
||||
|
||||
def get_cached_emoji_reactions(object) do
|
||||
if is_list(object.data["reactions"]) do
|
||||
object.data["reactions"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
Object.get_emoji_reactions(object)
|
||||
end
|
||||
|
||||
@spec add_like_to_object(Activity.t(), Object.t()) ::
|
||||
|
|
@ -489,17 +517,37 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
|
||||
def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
|
||||
%{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
|
||||
emoji = Pleroma.Emoji.maybe_quote(emoji)
|
||||
|
||||
"EmojiReact"
|
||||
|> Activity.Queries.by_type()
|
||||
|> where(actor: ^ap_id)
|
||||
|> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
|
||||
|> custom_emoji_discriminator(emoji)
|
||||
|> Activity.Queries.by_object_id(object_ap_id)
|
||||
|> order_by([activity], fragment("? desc nulls last", activity.id))
|
||||
|> limit(1)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
defp custom_emoji_discriminator(query, emoji) do
|
||||
if String.contains?(emoji, "@") do
|
||||
stripped = Pleroma.Emoji.maybe_strip_name(emoji)
|
||||
[name, domain] = String.split(stripped, "@")
|
||||
domain_pattern = "%/" <> domain <> "/%"
|
||||
emoji_pattern = Pleroma.Emoji.maybe_quote(name)
|
||||
|
||||
query
|
||||
|> where([activity], fragment("?->>'content' = ?
|
||||
AND EXISTS (
|
||||
SELECT FROM jsonb_array_elements(?->'tag') elem
|
||||
WHERE elem->>'id' ILIKE ?
|
||||
)", activity.data, ^emoji_pattern, activity.data, ^domain_pattern))
|
||||
else
|
||||
query
|
||||
|> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
|
||||
end
|
||||
end
|
||||
|
||||
#### Announce-related helpers
|
||||
|
||||
@doc """
|
||||
|
|
@ -673,14 +721,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
|
||||
#### Flag-related helpers
|
||||
@spec make_flag_data(map(), map()) :: map()
|
||||
def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
|
||||
def make_flag_data(
|
||||
%{actor: actor, context: context, content: content} = params,
|
||||
additional
|
||||
) do
|
||||
%{
|
||||
"type" => "Flag",
|
||||
"actor" => actor.ap_id,
|
||||
"content" => content,
|
||||
"object" => build_flag_object(params),
|
||||
"context" => context,
|
||||
"state" => "open"
|
||||
"state" => "open",
|
||||
"rules" => Map.get(params, :rules, nil)
|
||||
}
|
||||
|> Map.merge(additional)
|
||||
end
|
||||
|
|
@ -728,10 +780,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
build_flag_object(object)
|
||||
|
||||
nil ->
|
||||
if %Object{} = object = Object.get_by_ap_id(id) do
|
||||
build_flag_object(object)
|
||||
else
|
||||
%{"id" => id, "deleted" => true}
|
||||
case Object.get_by_ap_id(id) do
|
||||
%Object{} = object -> build_flag_object(object)
|
||||
_ -> %{"id" => id, "deleted" => true}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -805,9 +856,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
[actor | reported_activities] = activity.data["object"]
|
||||
|
||||
stripped_activities =
|
||||
Enum.map(reported_activities, fn
|
||||
act when is_map(act) -> act["id"]
|
||||
act when is_binary(act) -> act
|
||||
Enum.reduce(reported_activities, [], fn act, acc ->
|
||||
case ObjectID.cast(act) do
|
||||
{:ok, act} -> [act | acc]
|
||||
_ -> acc
|
||||
end
|
||||
end)
|
||||
|
||||
new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
|
||||
|
|
@ -885,4 +938,27 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def maybe_handle_group_posts(activity) do
|
||||
poster = User.get_cached_by_ap_id(activity.actor)
|
||||
|
||||
mentions =
|
||||
activity.data["to"]
|
||||
|> Enum.filter(&(&1 != activity.actor))
|
||||
|
||||
mentioned_local_groups =
|
||||
User.get_all_by_ap_id(mentions)
|
||||
|> Enum.filter(fn user ->
|
||||
user.actor_type == "Group" and
|
||||
user.local and
|
||||
not User.blocks?(user, poster)
|
||||
end)
|
||||
|
||||
mentioned_local_groups
|
||||
|> Enum.each(fn group ->
|
||||
Pleroma.Web.CommonAPI.repeat(activity.id, group)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"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.",
|
||||
|
|
@ -66,8 +67,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
def render("user.json", %{user: %User{nickname: nil} = user}),
|
||||
do: render("service.json", %{user: user})
|
||||
|
||||
def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
|
||||
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
|
||||
def render("user.json", %{user: %User{nickname: "internal." <> _} = user}) do
|
||||
render("service.json", %{user: user})
|
||||
|> Map.merge(%{
|
||||
"preferredUsername" => user.nickname,
|
||||
"webfinger" => "acct:#{User.full_nickname(user)}"
|
||||
})
|
||||
end
|
||||
|
||||
def render("user.json", %{user: user}) do
|
||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
|
|
@ -120,7 +126,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"discoverable" => user.is_discoverable,
|
||||
"capabilities" => capabilities,
|
||||
"alsoKnownAs" => user.also_known_as,
|
||||
"vcard:bday" => birthday
|
||||
"vcard:bday" => birthday,
|
||||
"webfinger" => "acct:#{User.full_nickname(user)}"
|
||||
}
|
||||
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|
||||
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|
||||
|
|
|
|||
|
|
@ -11,28 +11,28 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|
|||
|
||||
require Pleroma.Constants
|
||||
|
||||
@spec is_public?(Object.t() | Activity.t() | map()) :: boolean()
|
||||
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
|
||||
def is_public?(%Object{data: data}), do: is_public?(data)
|
||||
def is_public?(%Activity{data: %{"type" => "Move"}}), do: true
|
||||
def is_public?(%Activity{data: data}), do: is_public?(data)
|
||||
def is_public?(%{"directMessage" => true}), do: false
|
||||
@spec public?(Object.t() | Activity.t() | map()) :: boolean()
|
||||
def public?(%Object{data: %{"type" => "Tombstone"}}), do: false
|
||||
def public?(%Object{data: data}), do: public?(data)
|
||||
def public?(%Activity{data: %{"type" => "Move"}}), do: true
|
||||
def public?(%Activity{data: data}), do: public?(data)
|
||||
def public?(%{"directMessage" => true}), do: false
|
||||
|
||||
def is_public?(data) do
|
||||
def public?(data) do
|
||||
Utils.label_in_message?(Pleroma.Constants.as_public(), data) or
|
||||
Utils.label_in_message?(Utils.as_local_public(), data)
|
||||
end
|
||||
|
||||
def is_local_public?(%Object{data: data}), do: is_local_public?(data)
|
||||
def is_local_public?(%Activity{data: data}), do: is_local_public?(data)
|
||||
def local_public?(%Object{data: data}), do: local_public?(data)
|
||||
def local_public?(%Activity{data: data}), do: local_public?(data)
|
||||
|
||||
def is_local_public?(data) do
|
||||
def local_public?(data) do
|
||||
Utils.label_in_message?(Utils.as_local_public(), data) and
|
||||
not Utils.label_in_message?(Pleroma.Constants.as_public(), data)
|
||||
end
|
||||
|
||||
def is_private?(activity) do
|
||||
with false <- is_public?(activity),
|
||||
def private?(activity) do
|
||||
with false <- public?(activity),
|
||||
%User{follower_address: follower_address} <-
|
||||
User.get_cached_by_ap_id(activity.data["actor"]) do
|
||||
follower_address in activity.data["to"]
|
||||
|
|
@ -41,20 +41,20 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|
|||
end
|
||||
end
|
||||
|
||||
def is_announceable?(activity, user, public \\ true) do
|
||||
is_public?(activity) ||
|
||||
(!public && is_private?(activity) && activity.data["actor"] == user.ap_id)
|
||||
def announceable?(activity, user, public \\ true) do
|
||||
public?(activity) ||
|
||||
(!public && private?(activity) && activity.data["actor"] == user.ap_id)
|
||||
end
|
||||
|
||||
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
|
||||
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
|
||||
def direct?(%Activity{data: %{"directMessage" => true}}), do: true
|
||||
def direct?(%Object{data: %{"directMessage" => true}}), do: true
|
||||
|
||||
def is_direct?(activity) do
|
||||
!is_public?(activity) && !is_private?(activity)
|
||||
def direct?(activity) do
|
||||
!public?(activity) && !private?(activity)
|
||||
end
|
||||
|
||||
def is_list?(%{data: %{"listMessage" => _}}), do: true
|
||||
def is_list?(_), do: false
|
||||
def list?(%{data: %{"listMessage" => _}}), do: true
|
||||
def list?(_), do: false
|
||||
|
||||
@spec visible_for_user?(Object.t() | Activity.t() | nil, User.t() | nil) :: boolean()
|
||||
def visible_for_user?(%Object{data: %{"type" => "Tombstone"}}, _), do: false
|
||||
|
|
@ -77,7 +77,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|
|||
when module in [Activity, Object] do
|
||||
if restrict_unauthenticated_access?(message),
|
||||
do: false,
|
||||
else: is_public?(message) and not is_local_public?(message)
|
||||
else: public?(message) and not local_public?(message)
|
||||
end
|
||||
|
||||
def visible_for_user?(%{__struct__: module} = message, user)
|
||||
|
|
@ -86,8 +86,8 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|
|||
y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
|
||||
|
||||
user_is_local = user.local
|
||||
federatable = not is_local_public?(message)
|
||||
(is_public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable)
|
||||
federatable = not local_public?(message)
|
||||
(public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable)
|
||||
end
|
||||
|
||||
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
alias Pleroma.ConfigDB
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action == :update)
|
||||
|
||||
plug(
|
||||
|
|
@ -76,7 +76,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
json(conn, translate_descriptions(descriptions))
|
||||
end
|
||||
|
||||
def show(conn, %{only_db: true}) do
|
||||
def show(%{private: %{open_api_spex: %{params: %{only_db: true}}}} = conn, _) do
|
||||
with :ok <- configurable_from_database() do
|
||||
configs = Pleroma.Repo.all(ConfigDB)
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
end
|
||||
end
|
||||
|
||||
def update(%{body_params: %{configs: configs}} = conn, _) do
|
||||
def update(%{private: %{open_api_spex: %{body_params: %{configs: configs}}}} = conn, _) do
|
||||
with :ok <- configurable_from_database() do
|
||||
results =
|
||||
configs
|
||||
|
|
|
|||
|
|
@ -18,13 +18,24 @@ defmodule Pleroma.Web.AdminAPI.FrontendController do
|
|||
def index(conn, _params) do
|
||||
installed = installed()
|
||||
|
||||
# FIrst get frontends from config,
|
||||
# then add frontends that are installed but not in the config
|
||||
frontends =
|
||||
[:frontends, :available]
|
||||
|> Config.get([])
|
||||
Config.get([:frontends, :available], [])
|
||||
|> Enum.map(fn {name, desc} ->
|
||||
Map.put(desc, "installed", name in installed)
|
||||
desc
|
||||
|> Map.put("installed", name in installed)
|
||||
|> Map.put("installed_refs", installed_refs(name))
|
||||
end)
|
||||
|
||||
frontends =
|
||||
frontends ++
|
||||
(installed
|
||||
|> Enum.filter(fn n -> not Enum.any?(frontends, fn f -> f["name"] == n end) end)
|
||||
|> Enum.map(fn name ->
|
||||
%{"name" => name, "installed" => true, "installed_refs" => installed_refs(name)}
|
||||
end))
|
||||
|
||||
render(conn, "index.json", frontends: frontends)
|
||||
end
|
||||
|
||||
|
|
@ -43,4 +54,12 @@ defmodule Pleroma.Web.AdminAPI.FrontendController do
|
|||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def installed_refs(name) do
|
||||
if name in installed() do
|
||||
File.ls!(Path.join(Pleroma.Frontend.dir(), name))
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
|
|||
alias Pleroma.Web.Plugs.InstanceStatic
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
||||
|
||||
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
|
|||
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :show)
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action in [:update, :delete])
|
||||
|
||||
def show(conn, %{name: document_name}) do
|
||||
def show(%{private: %{open_api_spex: %{params: %{name: document_name}}}} = conn, _) do
|
||||
with {:ok, url} <- InstanceDocument.get(document_name),
|
||||
{:ok, content} <- File.read(InstanceStatic.file_path(url)) do
|
||||
conn
|
||||
|
|
@ -27,13 +27,18 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
|
|||
end
|
||||
end
|
||||
|
||||
def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do
|
||||
def update(
|
||||
%{
|
||||
private: %{open_api_spex: %{body_params: %{file: file}, params: %{name: document_name}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with {:ok, url} <- InstanceDocument.put(document_name, file.path) do
|
||||
json(conn, %{"url" => url})
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{name: document_name}) do
|
||||
def delete(%{private: %{open_api_spex: %{params: %{name: document_name}}}} = conn, _) do
|
||||
with :ok <- InstanceDocument.delete(document_name) do
|
||||
json(conn, %{})
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
|
|||
|
||||
require Logger
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:read:invites"]} when action == :index)
|
||||
|
||||
plug(
|
||||
|
|
@ -33,14 +33,14 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
|
|||
end
|
||||
|
||||
@doc "Create an account registration invite token"
|
||||
def create(%{body_params: params} = conn, _) do
|
||||
def create(%{private: %{open_api_spex: %{body_params: params}}} = conn, _) do
|
||||
{:ok, invite} = UserInviteToken.create_invite(params)
|
||||
|
||||
render(conn, "show.json", invite: invite)
|
||||
end
|
||||
|
||||
@doc "Revokes invite by token"
|
||||
def revoke(%{body_params: %{token: token}} = conn, _) do
|
||||
def revoke(%{private: %{open_api_spex: %{body_params: %{token: token}}}} = conn, _) do
|
||||
with {:ok, invite} <- UserInviteToken.find_by_token(token),
|
||||
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
|
||||
render(conn, "show.json", invite: updated_invite)
|
||||
|
|
@ -51,7 +51,13 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
|
|||
end
|
||||
|
||||
@doc "Sends registration invite via email"
|
||||
def email(%{assigns: %{user: user}, body_params: %{email: email} = params} = conn, _) do
|
||||
def email(
|
||||
%{
|
||||
assigns: %{user: user},
|
||||
private: %{open_api_spex: %{body_params: %{email: email} = params}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
|
||||
{_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
|
||||
{:ok, invite_token} <- UserInviteToken.create_invite(),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
|
|||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
|
|
@ -27,7 +27,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
|
|||
|
||||
defdelegate open_api_operation(action), to: Spec.MediaProxyCacheOperation
|
||||
|
||||
def index(%{assigns: %{user: _}} = conn, params) do
|
||||
def index(%{assigns: %{user: _}, private: %{open_api_spex: %{params: params}}} = conn, _) do
|
||||
entries = fetch_entries(params)
|
||||
urls = paginate_entries(entries, params.page, params.page_size)
|
||||
|
||||
|
|
@ -59,12 +59,19 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
|
|||
Enum.slice(entries, offset, page_size)
|
||||
end
|
||||
|
||||
def delete(%{assigns: %{user: _}, body_params: %{urls: urls}} = conn, _) do
|
||||
def delete(
|
||||
%{assigns: %{user: _}, private: %{open_api_spex: %{body_params: %{urls: urls}}}} = conn,
|
||||
_
|
||||
) do
|
||||
MediaProxy.remove_from_banned_urls(urls)
|
||||
json(conn, %{})
|
||||
end
|
||||
|
||||
def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: ban}} = conn, _) do
|
||||
def purge(
|
||||
%{assigns: %{user: _}, private: %{open_api_spex: %{body_params: %{urls: urls, ban: ban}}}} =
|
||||
conn,
|
||||
_
|
||||
) do
|
||||
MediaProxy.Invalidation.purge(urls)
|
||||
|
||||
if ban do
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
|
|||
|
||||
require Logger
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
|
|
@ -31,7 +31,13 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
|
|||
end
|
||||
end
|
||||
|
||||
def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do
|
||||
def follow(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{body_params: %{relay_url: target}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with {:ok, _message} <- Relay.follow(target) do
|
||||
ModerationLog.insert_log(%{action: "relay_follow", actor: admin, target: target})
|
||||
|
||||
|
|
@ -44,7 +50,13 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
|
|||
end
|
||||
end
|
||||
|
||||
def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target} = params} = conn, _) do
|
||||
def unfollow(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{body_params: %{relay_url: target} = params}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with {:ok, _message} <- Relay.unfollow(target, %{force: params[:force]}) do
|
||||
ModerationLog.insert_log(%{action: "relay_unfollow", actor: admin, target: target})
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
|
|||
|
||||
require Logger
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:read:reports"]} when action in [:index, :show])
|
||||
|
||||
plug(
|
||||
|
|
@ -31,13 +31,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
|
|||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ReportOperation
|
||||
|
||||
def index(conn, params) do
|
||||
def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
|
||||
reports = Utils.get_reports(params, params.page, params.page_size)
|
||||
|
||||
render(conn, "index.json", reports: reports)
|
||||
end
|
||||
|
||||
def show(conn, %{id: id}) do
|
||||
def show(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
|
||||
with %Activity{} = report <- Activity.get_report(id) do
|
||||
render(conn, "show.json", Report.extract_report_info(report))
|
||||
else
|
||||
|
|
@ -45,7 +45,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
|
|||
end
|
||||
end
|
||||
|
||||
def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn, _) do
|
||||
def update(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{body_params: %{reports: reports}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
result =
|
||||
Enum.map(reports, fn report ->
|
||||
case CommonAPI.update_report_state(report.id, report.state) do
|
||||
|
|
@ -73,9 +79,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
|
|||
end
|
||||
end
|
||||
|
||||
def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = conn, %{
|
||||
id: report_id
|
||||
}) do
|
||||
def notes_create(
|
||||
%{
|
||||
assigns: %{user: user},
|
||||
private: %{open_api_spex: %{body_params: %{content: content}, params: %{id: report_id}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with {:ok, _} <- ReportNote.create(user.id, report_id, content),
|
||||
report <- Activity.get_by_id_with_user_actor(report_id) do
|
||||
ModerationLog.insert_log(%{
|
||||
|
|
@ -92,10 +102,20 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
|
|||
end
|
||||
end
|
||||
|
||||
def notes_delete(%{assigns: %{user: user}} = conn, %{
|
||||
id: note_id,
|
||||
report_id: report_id
|
||||
}) do
|
||||
def notes_delete(
|
||||
%{
|
||||
assigns: %{user: user},
|
||||
private: %{
|
||||
open_api_spex: %{
|
||||
params: %{
|
||||
id: note_id,
|
||||
report_id: report_id
|
||||
}
|
||||
}
|
||||
}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with {:ok, note} <- ReportNote.destroy(note_id),
|
||||
report <- Activity.get_by_id_with_user_actor(report_id) do
|
||||
ModerationLog.insert_log(%{
|
||||
|
|
|
|||
62
lib/pleroma/web/admin_api/controllers/rule_controller.ex
Normal file
62
lib/pleroma/web/admin_api/controllers/rule_controller.ex
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.RuleController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Rule
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
import Pleroma.Web.ControllerHelper,
|
||||
only: [
|
||||
json_response: 3
|
||||
]
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["admin:write"]}
|
||||
when action in [:create, :update, :delete]
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :index)
|
||||
|
||||
action_fallback(AdminAPI.FallbackController)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RuleOperation
|
||||
|
||||
def index(conn, _) do
|
||||
rules =
|
||||
Rule.query()
|
||||
|> Repo.all()
|
||||
|
||||
render(conn, "index.json", rules: rules)
|
||||
end
|
||||
|
||||
def create(%{body_params: params} = conn, _) do
|
||||
rule =
|
||||
params
|
||||
|> Rule.create()
|
||||
|
||||
render(conn, "show.json", rule: rule)
|
||||
end
|
||||
|
||||
def update(%{body_params: params} = conn, %{id: id}) do
|
||||
rule =
|
||||
params
|
||||
|> Rule.update(id)
|
||||
|
||||
render(conn, "show.json", rule: rule)
|
||||
end
|
||||
|
||||
def delete(conn, %{id: id}) do
|
||||
with {:ok, _} <- Rule.delete(id) do
|
||||
json(conn, %{})
|
||||
else
|
||||
_ -> json_response(conn, :bad_request, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
|
||||
@users_page_size 50
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
|
|
@ -51,13 +51,22 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.UserOperation
|
||||
|
||||
def delete(conn, %{nickname: nickname}) do
|
||||
def delete(%{private: %{open_api_spex: %{params: %{nickname: nickname}}}} = conn, _) do
|
||||
conn
|
||||
|> Map.put(:body_params, %{nicknames: [nickname]})
|
||||
|> delete(%{})
|
||||
|> do_deletes([nickname])
|
||||
end
|
||||
|
||||
def delete(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
|
||||
def delete(
|
||||
%{
|
||||
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
conn
|
||||
|> do_deletes(nicknames)
|
||||
end
|
||||
|
||||
defp do_deletes(%{assigns: %{user: admin}} = conn, nicknames) when is_list(nicknames) do
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
|
||||
Enum.each(users, fn user ->
|
||||
|
|
@ -77,9 +86,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
def follow(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
body_params: %{
|
||||
follower: follower_nick,
|
||||
followed: followed_nick
|
||||
private: %{
|
||||
open_api_spex: %{
|
||||
body_params: %{
|
||||
follower: follower_nick,
|
||||
followed: followed_nick
|
||||
}
|
||||
}
|
||||
}
|
||||
} = conn,
|
||||
_
|
||||
|
|
@ -102,9 +115,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
def unfollow(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
body_params: %{
|
||||
follower: follower_nick,
|
||||
followed: followed_nick
|
||||
private: %{
|
||||
open_api_spex: %{
|
||||
body_params: %{
|
||||
follower: follower_nick,
|
||||
followed: followed_nick
|
||||
}
|
||||
}
|
||||
}
|
||||
} = conn,
|
||||
_
|
||||
|
|
@ -124,7 +141,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
json(conn, "ok")
|
||||
end
|
||||
|
||||
def create(%{assigns: %{user: admin}, body_params: %{users: users}} = conn, _) do
|
||||
def create(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{body_params: %{users: users}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
changesets =
|
||||
users
|
||||
|> Enum.map(fn %{nickname: nickname, email: email, password: password} ->
|
||||
|
|
@ -178,7 +201,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
end
|
||||
end
|
||||
|
||||
def show(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do
|
||||
def show(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{params: %{nickname: nickname}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
|
||||
render(conn, "show.json", %{user: user})
|
||||
else
|
||||
|
|
@ -186,7 +215,11 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
end
|
||||
end
|
||||
|
||||
def toggle_activation(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do
|
||||
def toggle_activation(
|
||||
%{assigns: %{user: admin}, private: %{open_api_spex: %{params: %{nickname: nickname}}}} =
|
||||
conn,
|
||||
_
|
||||
) do
|
||||
user = User.get_cached_by_nickname(nickname)
|
||||
|
||||
{:ok, updated_user} = User.set_activation(user, !user.is_active)
|
||||
|
|
@ -202,7 +235,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
render(conn, "show.json", user: updated_user)
|
||||
end
|
||||
|
||||
def activate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
|
||||
def activate(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
{:ok, updated_users} = User.set_activation(users, true)
|
||||
|
||||
|
|
@ -212,10 +251,16 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
action: "activate"
|
||||
})
|
||||
|
||||
render(conn, "index.json", users: Keyword.values(updated_users))
|
||||
render(conn, "index.json", users: updated_users)
|
||||
end
|
||||
|
||||
def deactivate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
|
||||
def deactivate(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
{:ok, updated_users} = User.set_activation(users, false)
|
||||
|
||||
|
|
@ -225,10 +270,16 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
action: "deactivate"
|
||||
})
|
||||
|
||||
render(conn, "index.json", users: Keyword.values(updated_users))
|
||||
render(conn, "index.json", users: updated_users)
|
||||
end
|
||||
|
||||
def approve(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
|
||||
def approve(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
{:ok, updated_users} = User.approve(users)
|
||||
|
||||
|
|
@ -241,7 +292,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
render(conn, "index.json", users: updated_users)
|
||||
end
|
||||
|
||||
def suggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
|
||||
def suggest(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
{:ok, updated_users} = User.set_suggestion(users, true)
|
||||
|
||||
|
|
@ -254,7 +311,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
render(conn, "index.json", users: updated_users)
|
||||
end
|
||||
|
||||
def unsuggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
|
||||
def unsuggest(
|
||||
%{
|
||||
assigns: %{user: admin},
|
||||
private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
{:ok, updated_users} = User.set_suggestion(users, false)
|
||||
|
||||
|
|
@ -267,7 +330,7 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
render(conn, "index.json", users: updated_users)
|
||||
end
|
||||
|
||||
def index(conn, params) do
|
||||
def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
|
||||
{page, page_size} = page_params(params)
|
||||
filters = maybe_parse_filters(params[:filters])
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ defmodule Pleroma.Web.AdminAPI.FrontendView do
|
|||
git: frontend["git"],
|
||||
build_url: frontend["build_url"],
|
||||
ref: frontend["ref"],
|
||||
installed: frontend["installed"]
|
||||
installed: frontend["installed"],
|
||||
installed_refs: frontend["installed_refs"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
|
|||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Rule
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.AdminAPI
|
||||
alias Pleroma.Web.AdminAPI.Report
|
||||
alias Pleroma.Web.AdminAPI.RuleView
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
|
|
@ -46,7 +48,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
|
|||
as: :activity
|
||||
}),
|
||||
state: report.data["state"],
|
||||
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
|
||||
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}),
|
||||
rules: rules(Map.get(report.data, "rules", nil))
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -71,4 +74,16 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
|
|||
created_at: Utils.to_masto_date(inserted_at)
|
||||
}
|
||||
end
|
||||
|
||||
defp rules(nil) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp rules(rule_ids) do
|
||||
rules =
|
||||
rule_ids
|
||||
|> Rule.get()
|
||||
|
||||
render(RuleView, "index.json", rules: rules)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
22
lib/pleroma/web/admin_api/views/rule_view.ex
Normal file
22
lib/pleroma/web/admin_api/views/rule_view.ex
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.RuleView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
def render("index.json", %{rules: rules} = _opts) do
|
||||
render_many(rules, __MODULE__, "show.json")
|
||||
end
|
||||
|
||||
def render("show.json", %{rule: rule} = _opts) do
|
||||
%{
|
||||
id: to_string(rule.id),
|
||||
priority: rule.priority,
|
||||
text: rule.text,
|
||||
hint: rule.hint
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -10,6 +10,14 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
|
||||
@behaviour OpenApi
|
||||
|
||||
defp streaming_paths do
|
||||
%{
|
||||
"/api/v1/streaming" => %OpenApiSpex.PathItem{
|
||||
get: Pleroma.Web.ApiSpec.StreamingOperation.streaming_operation()
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@impl OpenApi
|
||||
def spec(opts \\ []) do
|
||||
%OpenApi{
|
||||
|
|
@ -35,7 +43,7 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
- [Mastodon API documentation](https://docs.joinmastodon.org/client/intro/)
|
||||
- [Differences in Mastodon API responses from vanilla Mastodon](https://docs-develop.pleroma.social/backend/development/API/differences_in_mastoapi_responses/)
|
||||
|
||||
Please report such occurences on our [issue tracker](https://git.pleroma.social/pleroma/pleroma/-/issues). Feel free to submit API questions or proposals there too!
|
||||
Please report such occurrences on our [issue tracker](https://git.pleroma.social/pleroma/pleroma/-/issues). Feel free to submit API questions or proposals there too!
|
||||
""",
|
||||
# Strip environment from the version
|
||||
version: Application.spec(:pleroma, :vsn) |> to_string() |> String.replace(~r/\+.*$/, ""),
|
||||
|
|
@ -45,7 +53,7 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
}
|
||||
},
|
||||
# populate the paths from a phoenix router
|
||||
paths: OpenApiSpex.Paths.from_router(Router),
|
||||
paths: Map.merge(streaming_paths(), OpenApiSpex.Paths.from_router(Router)),
|
||||
components: %OpenApiSpex.Components{
|
||||
parameters: %{
|
||||
"accountIdOrNickname" =>
|
||||
|
|
@ -86,14 +94,15 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"tags" => [
|
||||
"Chat administration",
|
||||
"Emoji pack administration",
|
||||
"Frontend managment",
|
||||
"Frontend management",
|
||||
"Instance configuration",
|
||||
"Instance documents",
|
||||
"Instance rule managment",
|
||||
"Invites",
|
||||
"MediaProxy cache",
|
||||
"OAuth application managment",
|
||||
"OAuth application management",
|
||||
"Relays",
|
||||
"Report managment",
|
||||
"Report management",
|
||||
"Status administration",
|
||||
"User administration",
|
||||
"Announcement management"
|
||||
|
|
@ -129,7 +138,8 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"Scheduled statuses",
|
||||
"Search",
|
||||
"Status actions",
|
||||
"Media attachments"
|
||||
"Media attachments",
|
||||
"Bookmark folders"
|
||||
]
|
||||
},
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -27,10 +27,12 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
|||
|
||||
@impl Plug
|
||||
|
||||
def call(conn, %{operation_id: operation_id, render_error: render_error}) do
|
||||
def call(conn, %{operation_id: operation_id, render_error: render_error} = opts) do
|
||||
{spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)
|
||||
operation = operation_lookup[operation_id]
|
||||
|
||||
cast_opts = opts |> Map.take([:replace_params]) |> Map.to_list()
|
||||
|
||||
content_type =
|
||||
case Conn.get_req_header(conn, "content-type") do
|
||||
[header_value | _] ->
|
||||
|
|
@ -44,7 +46,7 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
|||
|
||||
conn = Conn.put_private(conn, :operation_id, operation_id)
|
||||
|
||||
case cast_and_validate(spec, operation, conn, content_type, strict?()) do
|
||||
case cast_and_validate(spec, operation, conn, content_type, strict?(), cast_opts) do
|
||||
{:ok, conn} ->
|
||||
conn
|
||||
|
||||
|
|
@ -94,11 +96,11 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
|||
|
||||
def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
|
||||
|
||||
defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
|
||||
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
|
||||
defp cast_and_validate(spec, operation, conn, content_type, true = _strict, cast_opts) do
|
||||
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
|
||||
end
|
||||
|
||||
defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
|
||||
defp cast_and_validate(spec, operation, conn, content_type, false = _strict, cast_opts) do
|
||||
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
|
||||
{:ok, conn} ->
|
||||
{:ok, conn}
|
||||
|
|
@ -123,7 +125,7 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
|||
end)
|
||||
|
||||
conn = %Conn{conn | query_params: query_params}
|
||||
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
|
||||
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
|
|||
Operation.parameter(
|
||||
:with_relationships,
|
||||
:query,
|
||||
BooleanLike,
|
||||
BooleanLike.schema(),
|
||||
"Embed relationships into accounts. **If this parameter is not set account's `pleroma.relationship` is going to be `null`.**"
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
alias Pleroma.Web.ApiSpec.Schemas.ActorType
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.List
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||
|
|
@ -122,22 +123,27 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
parameters:
|
||||
[
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||
Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"),
|
||||
Operation.parameter(
|
||||
:pinned,
|
||||
:query,
|
||||
BooleanLike.schema(),
|
||||
"Include only pinned statuses"
|
||||
),
|
||||
Operation.parameter(:tagged, :query, :string, "With tag"),
|
||||
Operation.parameter(
|
||||
:only_media,
|
||||
:query,
|
||||
BooleanLike,
|
||||
BooleanLike.schema(),
|
||||
"Include only statuses with media attached"
|
||||
),
|
||||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike,
|
||||
BooleanLike.schema(),
|
||||
"Include statuses from muted accounts."
|
||||
),
|
||||
Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
|
||||
Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
|
||||
Operation.parameter(:exclude_reblogs, :query, BooleanLike.schema(), "Exclude reblogs"),
|
||||
Operation.parameter(:exclude_replies, :query, BooleanLike.schema(), "Exclude replies"),
|
||||
Operation.parameter(
|
||||
:exclude_visibilities,
|
||||
:query,
|
||||
|
|
@ -147,7 +153,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike,
|
||||
BooleanLike.schema(),
|
||||
"Include reactions from muted accounts."
|
||||
)
|
||||
] ++ pagination_params(),
|
||||
|
|
@ -347,7 +353,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
summary: "Endorse",
|
||||
operationId: "AccountController.endorse",
|
||||
security: [%{"oAuth" => ["follow", "write:accounts"]}],
|
||||
description: "Addds the given account to endorsed accounts list.",
|
||||
description: "Adds the given account to endorsed accounts list.",
|
||||
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship),
|
||||
|
|
@ -452,7 +458,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
operationId: "AccountController.blocks",
|
||||
description: "View your blocks. See also accounts/:id/{block,unblock}",
|
||||
security: [%{"oAuth" => ["read:blocks"]}],
|
||||
parameters: pagination_params(),
|
||||
parameters: [with_relationships_param() | pagination_params()],
|
||||
responses: %{
|
||||
200 => Operation.response("Accounts", "application/json", array_of_accounts())
|
||||
}
|
||||
|
|
@ -508,6 +514,48 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def familiar_followers_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve account information"],
|
||||
summary: "Followers that you follow",
|
||||
operationId: "AccountController.familiar_followers",
|
||||
description:
|
||||
"Obtain a list of all accounts that follow a given account, filtered for accounts you follow.",
|
||||
security: [%{"oAuth" => ["read:follows"]}],
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:id,
|
||||
:query,
|
||||
%Schema{
|
||||
oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}]
|
||||
},
|
||||
"Account IDs",
|
||||
example: "123"
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Accounts", "application/json", %Schema{
|
||||
title: "ArrayOfAccounts",
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
title: "Account",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: FlakeID,
|
||||
accounts: %Schema{
|
||||
title: "ArrayOfAccounts",
|
||||
type: :array,
|
||||
items: Account,
|
||||
example: [Account.schema().example]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "AccountCreateRequest",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.FrontendOperation do
|
|||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Frontend managment"],
|
||||
tags: ["Frontend management"],
|
||||
summary: "Retrieve a list of available frontends",
|
||||
operationId: "AdminAPI.FrontendController.index",
|
||||
security: [%{"oAuth" => ["admin:read"]}],
|
||||
|
|
@ -29,7 +29,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.FrontendOperation do
|
|||
|
||||
def install_operation do
|
||||
%Operation{
|
||||
tags: ["Frontend managment"],
|
||||
tags: ["Frontend management"],
|
||||
summary: "Install a frontend",
|
||||
operationId: "AdminAPI.FrontendController.install",
|
||||
security: [%{"oAuth" => ["admin:read"]}],
|
||||
|
|
@ -51,8 +51,9 @@ defmodule Pleroma.Web.ApiSpec.Admin.FrontendOperation do
|
|||
name: %Schema{type: :string},
|
||||
git: %Schema{type: :string, format: :uri, nullable: true},
|
||||
build_url: %Schema{type: :string, format: :uri, nullable: true},
|
||||
ref: %Schema{type: :string},
|
||||
installed: %Schema{type: :boolean}
|
||||
ref: %Schema{type: :string, nullable: true},
|
||||
installed: %Schema{type: :boolean},
|
||||
installed_refs: %Schema{type: :array, items: %Schema{type: :string}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
|
|||
def index_operation do
|
||||
%Operation{
|
||||
summary: "Retrieve a list of OAuth applications",
|
||||
tags: ["OAuth application managment"],
|
||||
tags: ["OAuth application management"],
|
||||
operationId: "AdminAPI.OAuthAppController.index",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: [
|
||||
|
|
@ -69,7 +69,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
|
|||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["OAuth application managment"],
|
||||
tags: ["OAuth application management"],
|
||||
summary: "Create an OAuth application",
|
||||
operationId: "AdminAPI.OAuthAppController.create",
|
||||
requestBody: request_body("Parameters", create_request()),
|
||||
|
|
@ -84,7 +84,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
|
|||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["OAuth application managment"],
|
||||
tags: ["OAuth application management"],
|
||||
summary: "Update OAuth application",
|
||||
operationId: "AdminAPI.OAuthAppController.update",
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
|
|
@ -102,7 +102,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
|
|||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["OAuth application managment"],
|
||||
tags: ["OAuth application management"],
|
||||
summary: "Delete OAuth application",
|
||||
operationId: "AdminAPI.OAuthAppController.delete",
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Report managment"],
|
||||
tags: ["Report management"],
|
||||
summary: "Retrieve a list of reports",
|
||||
operationId: "AdminAPI.ReportController.index",
|
||||
security: [%{"oAuth" => ["admin:read:reports"]}],
|
||||
|
|
@ -30,6 +30,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
report_state(),
|
||||
"Filter by report state"
|
||||
),
|
||||
Operation.parameter(
|
||||
:rule_id,
|
||||
:query,
|
||||
%Schema{type: :string},
|
||||
"Filter by selected rule id"
|
||||
),
|
||||
Operation.parameter(
|
||||
:limit,
|
||||
:query,
|
||||
|
|
@ -69,7 +75,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Report managment"],
|
||||
tags: ["Report management"],
|
||||
summary: "Retrieve a report",
|
||||
operationId: "AdminAPI.ReportController.show",
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
|
|
@ -83,7 +89,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Report managment"],
|
||||
tags: ["Report management"],
|
||||
summary: "Change state of specified reports",
|
||||
operationId: "AdminAPI.ReportController.update",
|
||||
security: [%{"oAuth" => ["admin:write:reports"]}],
|
||||
|
|
@ -99,7 +105,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
|
||||
def notes_create_operation do
|
||||
%Operation{
|
||||
tags: ["Report managment"],
|
||||
tags: ["Report management"],
|
||||
summary: "Add a note to the report",
|
||||
operationId: "AdminAPI.ReportController.notes_create",
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
|
|
@ -120,7 +126,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
|
||||
def notes_delete_operation do
|
||||
%Operation{
|
||||
tags: ["Report managment"],
|
||||
tags: ["Report management"],
|
||||
summary: "Delete note attached to the report",
|
||||
operationId: "AdminAPI.ReportController.notes_delete",
|
||||
parameters: [
|
||||
|
|
@ -141,7 +147,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
end
|
||||
|
||||
def id_param do
|
||||
Operation.parameter(:id, :path, FlakeID, "Report ID",
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Report ID",
|
||||
example: "9umDrYheeY451cQnEe",
|
||||
required: true
|
||||
)
|
||||
|
|
@ -169,6 +175,17 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
inserted_at: %Schema{type: :string, format: :"date-time"}
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
text: %Schema{type: :string},
|
||||
hint: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
115
lib/pleroma/web/api_spec/operations/admin/rule_operation.ex
Normal file
115
lib/pleroma/web/api_spec/operations/admin/rule_operation.ex
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Instance rule managment"],
|
||||
summary: "Retrieve list of instance rules",
|
||||
operationId: "AdminAPI.RuleController.index",
|
||||
security: [%{"oAuth" => ["admin:read"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Response", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: rule()
|
||||
}),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Instance rule managment"],
|
||||
summary: "Create new rule",
|
||||
operationId: "AdminAPI.RuleController.create",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody: request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Response", "application/json", rule()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Instance rule managment"],
|
||||
summary: "Modify existing rule",
|
||||
operationId: "AdminAPI.RuleController.update",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
|
||||
requestBody: request_body("Parameters", update_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Response", "application/json", rule()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Instance rule managment"],
|
||||
summary: "Delete rule",
|
||||
operationId: "AdminAPI.RuleController.delete",
|
||||
parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
responses: %{
|
||||
200 => empty_object_response(),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:text],
|
||||
properties: %{
|
||||
priority: %Schema{type: :integer},
|
||||
text: %Schema{type: :string},
|
||||
hint: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
priority: %Schema{type: :integer},
|
||||
text: %Schema{type: :string},
|
||||
hint: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp rule do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
priority: %Schema{type: :integer},
|
||||
text: %Schema{type: :string},
|
||||
hint: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -137,7 +137,12 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
|
|||
"Deprecated due to no support for pagination. Using [/api/v2/pleroma/chats](#operation/ChatController.index2) instead is recommended.",
|
||||
operationId: "ChatController.index",
|
||||
parameters: [
|
||||
Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
|
||||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike.schema(),
|
||||
"Include chats from muted users"
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("The chats of the user", "application/json", chats_response())
|
||||
|
|
@ -156,7 +161,12 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
|
|||
summary: "Retrieve list of chats",
|
||||
operationId: "ChatController.index2",
|
||||
parameters: [
|
||||
Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
|
||||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike.schema(),
|
||||
"Include chats from muted users"
|
||||
)
|
||||
| pagination_params()
|
||||
],
|
||||
responses: %{
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ defmodule Pleroma.Web.ApiSpec.DirectoryOperation do
|
|||
"Order by recent activity or account creation",
|
||||
required: nil
|
||||
),
|
||||
Operation.parameter(:local, :query, BooleanLike, "Include local users only")
|
||||
Operation.parameter(:local, :query, BooleanLike.schema(), "Include local users only")
|
||||
] ++ pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
|
|||
summary:
|
||||
"Get an object of emoji to account mappings with accounts that reacted to the post",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Status ID", required: true),
|
||||
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
|
||||
required: nil
|
||||
),
|
||||
|
|
@ -45,7 +45,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
|
|||
tags: ["Emoji reactions"],
|
||||
summary: "React to a post with a unicode emoji",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Status ID", required: true),
|
||||
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
|
||||
required: true
|
||||
)
|
||||
|
|
@ -64,7 +64,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
|
|||
tags: ["Emoji reactions"],
|
||||
summary: "Remove a reaction to a post with a unicode emoji",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Status ID", required: true),
|
||||
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
|
||||
required: true
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def show2_operation do
|
||||
%Operation{
|
||||
tags: ["Instance misc"],
|
||||
summary: "Retrieve instance information",
|
||||
description: "Information about the server",
|
||||
operationId: "InstanceController.show2",
|
||||
responses: %{
|
||||
200 => Operation.response("Instance", "application/json", instance2())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def peers_operation do
|
||||
%Operation{
|
||||
tags: ["Instance misc"],
|
||||
|
|
@ -34,10 +46,30 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def rules_operation do
|
||||
%Operation{
|
||||
tags: ["Instance misc"],
|
||||
summary: "Retrieve list of instance rules",
|
||||
operationId: "InstanceController.rules",
|
||||
responses: %{
|
||||
200 => Operation.response("Array of domains", "application/json", array_of_rules())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp instance do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
accounts: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
max_featured_tags: %Schema{
|
||||
type: :integer,
|
||||
description: "The maximum number of featured tags allowed for each account."
|
||||
}
|
||||
}
|
||||
},
|
||||
uri: %Schema{type: :string, description: "The domain name of the instance"},
|
||||
title: %Schema{type: :string, description: "The title of the website"},
|
||||
description: %Schema{
|
||||
|
|
@ -89,7 +121,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
languages: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Primary langauges of the website and its staff"
|
||||
description: "Primary languages of the website and its staff"
|
||||
},
|
||||
registrations: %Schema{type: :boolean, description: "Whether registrations are enabled"},
|
||||
# Extra (not present in Mastodon):
|
||||
|
|
@ -160,7 +192,186 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
"urls" => %{
|
||||
"streaming_api" => "wss://lain.com"
|
||||
},
|
||||
"version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
|
||||
"version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)",
|
||||
"rules" => array_of_rules()
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp instance2 do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
domain: %Schema{type: :string, description: "The domain name of the instance"},
|
||||
title: %Schema{type: :string, description: "The title of the website"},
|
||||
version: %Schema{
|
||||
type: :string,
|
||||
description: "The version of Pleroma installed on the instance"
|
||||
},
|
||||
source_url: %Schema{
|
||||
type: :string,
|
||||
description: "The version of Pleroma installed on the instance"
|
||||
},
|
||||
description: %Schema{
|
||||
type: :string,
|
||||
description: "Admin-defined description of the Pleroma site"
|
||||
},
|
||||
usage: %Schema{
|
||||
type: :object,
|
||||
description: "Instance usage statistics",
|
||||
properties: %{
|
||||
users: %Schema{
|
||||
type: :object,
|
||||
description: "User count statistics",
|
||||
properties: %{
|
||||
active_month: %Schema{
|
||||
type: :integer,
|
||||
description: "Monthly active users"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
email: %Schema{
|
||||
type: :string,
|
||||
description: "An email that may be contacted for any inquiries",
|
||||
format: :email
|
||||
},
|
||||
urls: %Schema{
|
||||
type: :object,
|
||||
description: "URLs of interest for clients apps",
|
||||
properties: %{}
|
||||
},
|
||||
stats: %Schema{
|
||||
type: :object,
|
||||
description: "Statistics about how much information the instance contains",
|
||||
properties: %{
|
||||
user_count: %Schema{
|
||||
type: :integer,
|
||||
description: "Users registered on this instance"
|
||||
},
|
||||
status_count: %Schema{
|
||||
type: :integer,
|
||||
description: "Statuses authored by users on instance"
|
||||
},
|
||||
domain_count: %Schema{
|
||||
type: :integer,
|
||||
description: "Domains federated with this instance"
|
||||
}
|
||||
}
|
||||
},
|
||||
thumbnail: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
url: %Schema{
|
||||
type: :string,
|
||||
description: "Banner image for the website",
|
||||
nullable: true
|
||||
}
|
||||
}
|
||||
},
|
||||
languages: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Primary languages of the website and its staff"
|
||||
},
|
||||
registrations: %Schema{
|
||||
type: :object,
|
||||
description: "Registrations-related configuration",
|
||||
properties: %{
|
||||
enabled: %Schema{
|
||||
type: :boolean,
|
||||
description: "Whether registrations are enabled"
|
||||
},
|
||||
approval_required: %Schema{
|
||||
type: :boolean,
|
||||
description: "Whether users need to be manually approved by admin"
|
||||
}
|
||||
}
|
||||
},
|
||||
configuration: %Schema{
|
||||
type: :object,
|
||||
description: "Instance configuration",
|
||||
properties: %{
|
||||
accounts: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
max_featured_tags: %Schema{
|
||||
type: :integer,
|
||||
description: "The maximum number of featured tags allowed for each account."
|
||||
},
|
||||
max_pinned_statuses: %Schema{
|
||||
type: :integer,
|
||||
description: "The maximum number of pinned statuses for each account."
|
||||
}
|
||||
}
|
||||
},
|
||||
urls: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
streaming: %Schema{
|
||||
type: :string,
|
||||
description: "Websockets address for push streaming"
|
||||
}
|
||||
}
|
||||
},
|
||||
statuses: %Schema{
|
||||
type: :object,
|
||||
description: "A map with poll limits for local statuses",
|
||||
properties: %{
|
||||
characters_reserved_per_url: %Schema{
|
||||
type: :integer,
|
||||
description:
|
||||
"Each URL in a status will be assumed to be exactly this many characters."
|
||||
},
|
||||
max_characters: %Schema{
|
||||
type: :integer,
|
||||
description: "Posts character limit (CW/Subject included in the counter)"
|
||||
},
|
||||
max_media_attachments: %Schema{
|
||||
type: :integer,
|
||||
description: "Media attachment limit"
|
||||
}
|
||||
}
|
||||
},
|
||||
media_attachments: %Schema{
|
||||
type: :object,
|
||||
description: "A map with poll limits for media attachments",
|
||||
properties: %{
|
||||
image_size_limit: %Schema{
|
||||
type: :integer,
|
||||
description: "File size limit of uploaded images"
|
||||
},
|
||||
video_size_limit: %Schema{
|
||||
type: :integer,
|
||||
description: "File size limit of uploaded videos"
|
||||
}
|
||||
}
|
||||
},
|
||||
polls: %Schema{
|
||||
type: :object,
|
||||
description: "A map with poll limits for local polls",
|
||||
properties: %{
|
||||
max_options: %Schema{
|
||||
type: :integer,
|
||||
description: "Maximum number of options."
|
||||
},
|
||||
max_characters_per_option: %Schema{
|
||||
type: :integer,
|
||||
description: "Maximum number of characters per option."
|
||||
},
|
||||
min_expiration: %Schema{
|
||||
type: :integer,
|
||||
description: "Minimum expiration time (in seconds)."
|
||||
},
|
||||
max_expiration: %Schema{
|
||||
type: :integer,
|
||||
description: "Maximum expiration time (in seconds)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -172,4 +383,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
example: ["pleroma.site", "lain.com", "bikeshed.party"]
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_rules do
|
||||
%Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
text: %Schema{type: :string},
|
||||
hint: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
|||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike,
|
||||
BooleanLike.schema(),
|
||||
"Include the notifications from muted users"
|
||||
)
|
||||
] ++ pagination_params(),
|
||||
|
|
@ -202,7 +202,8 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
|||
"pleroma:report",
|
||||
"move",
|
||||
"follow_request",
|
||||
"poll"
|
||||
"poll",
|
||||
"status"
|
||||
],
|
||||
description: """
|
||||
The type of event that resulted in the notification.
|
||||
|
|
@ -216,6 +217,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
|||
- `pleroma:emoji_reaction` - Someone reacted with emoji to your status
|
||||
- `pleroma:chat_mention` - Someone mentioned you in a chat message
|
||||
- `pleroma:report` - Someone was reported
|
||||
- `status` - Someone you are subscribed to created a status
|
||||
"""
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
|
|||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, FlakeID, "Account ID",
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Account ID",
|
||||
example: "9umDrYheeY451cQnEe",
|
||||
required: true
|
||||
)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,13 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
|
|||
content_type: %Schema{type: :string},
|
||||
file_name: %Schema{type: :string},
|
||||
file_size: %Schema{type: :integer},
|
||||
processed: %Schema{type: :boolean}
|
||||
processed: %Schema{type: :boolean, description: "whether this backup has succeeded"},
|
||||
state: %Schema{
|
||||
type: :string,
|
||||
description: "the state of the backup",
|
||||
enum: ["pending", "running", "complete", "failed"]
|
||||
},
|
||||
processed_number: %Schema{type: :integer, description: "the number of records processed"}
|
||||
},
|
||||
example: %{
|
||||
"content_type" => "application/zip",
|
||||
|
|
@ -72,7 +78,9 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
|
|||
"https://cofe.fe:4000/media/backups/archive-foobar-20200908T164207-Yr7vuT5Wycv-sN3kSN2iJ0k-9pMo60j9qmvRCdDqIew.zip",
|
||||
"file_size" => 4105,
|
||||
"inserted_at" => "2020-09-08T16:42:07.000Z",
|
||||
"processed" => true
|
||||
"processed" => true,
|
||||
"state" => "complete",
|
||||
"processed_number" => 20
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
# 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.PleromaBookmarkFolderOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BookmarkFolder
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
@spec open_api_operation(any()) :: any()
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Bookmark folders"],
|
||||
summary: "All bookmark folders",
|
||||
security: [%{"oAuth" => ["read:bookmarks"]}],
|
||||
operationId: "PleromaAPI.BookmarkFolderController.index",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Bookmark Folders", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: BookmarkFolder
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Bookmark folders"],
|
||||
summary: "Create a bookmark folder",
|
||||
security: [%{"oAuth" => ["write:bookmarks"]}],
|
||||
operationId: "PleromaAPI.BookmarkFolderController.create",
|
||||
requestBody: request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
|
||||
422 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Bookmark folders"],
|
||||
summary: "Update a bookmark folder",
|
||||
security: [%{"oAuth" => ["write:bookmarks"]}],
|
||||
operationId: "PleromaAPI.BookmarkFolderController.update",
|
||||
parameters: [id_param()],
|
||||
requestBody: request_body("Parameters", update_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
422 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Bookmark folders"],
|
||||
summary: "Delete a bookmark folder",
|
||||
security: [%{"oAuth" => ["write:bookmarks"]}],
|
||||
operationId: "PleromaAPI.BookmarkFolderController.delete",
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "BookmarkFolderCreateRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{
|
||||
type: :string,
|
||||
description: "Folder name"
|
||||
},
|
||||
emoji: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Folder emoji"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
title: "BookmarkFolderUpdateRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Folder name"
|
||||
},
|
||||
emoji: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Folder emoji"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def id_param do
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Bookmark Folder ID",
|
||||
example: "9umDrYheeY451cQnEe",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.NotificationOperation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
|
@ -35,12 +34,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
|
|||
Operation.response(
|
||||
"A Notification or array of Notifications",
|
||||
"application/json",
|
||||
%Schema{
|
||||
anyOf: [
|
||||
%Schema{type: :array, items: NotificationOperation.notification()},
|
||||
NotificationOperation.notification()
|
||||
]
|
||||
}
|
||||
%Schema{type: :string}
|
||||
),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
|||
summary: "Creates a new Listen activity for an account",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
operationId: "PleromaAPI.ScrobbleController.create",
|
||||
requestBody: request_body("Parameters", create_request(), requried: true),
|
||||
deprecated: true,
|
||||
requestBody: request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Scrobble", "application/json", scrobble())
|
||||
}
|
||||
|
|
@ -34,6 +35,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
|||
tags: ["Scrobbles"],
|
||||
summary: "Requests a list of current and recent Listen activities for an account",
|
||||
operationId: "PleromaAPI.ScrobbleController.index",
|
||||
deprecated: true,
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params()
|
||||
],
|
||||
|
|
@ -57,6 +59,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
|||
album: %Schema{type: :string, description: "The album of the media playing"},
|
||||
artist: %Schema{type: :string, description: "The artist of the media playing"},
|
||||
length: %Schema{type: :integer, description: "The length of the media playing"},
|
||||
externalLink: %Schema{type: :string, description: "A URL referencing the media playing"},
|
||||
visibility: %Schema{
|
||||
allOf: [VisibilityScope],
|
||||
default: "public",
|
||||
|
|
@ -67,7 +70,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
|||
"title" => "Some Title",
|
||||
"artist" => "Some Artist",
|
||||
"album" => "Some Album",
|
||||
"length" => 180_000
|
||||
"length" => 180_000,
|
||||
"externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -81,6 +85,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
|||
title: %Schema{type: :string, description: "The title of the media playing"},
|
||||
album: %Schema{type: :string, description: "The album of the media playing"},
|
||||
artist: %Schema{type: :string, description: "The artist of the media playing"},
|
||||
externalLink: %Schema{type: :string, description: "A URL referencing the media playing"},
|
||||
length: %Schema{
|
||||
type: :integer,
|
||||
description: "The length of the media playing",
|
||||
|
|
@ -95,6 +100,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
|||
"artist" => "Some Artist",
|
||||
"album" => "Some Album",
|
||||
"length" => 180_000,
|
||||
"externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title",
|
||||
"created_at" => "2019-09-28T12:40:45.000Z"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
# 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.PleromaStatusOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.StatusOperation
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def quotes_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve status information"],
|
||||
summary: "Quoted by",
|
||||
description: "View quotes for a given status",
|
||||
operationId: "PleromaAPI.StatusController.quotes",
|
||||
parameters: [id_param() | pagination_params()],
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"Array of Status",
|
||||
"application/json",
|
||||
StatusOperation.array_of_statuses()
|
||||
),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def id_param do
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Status ID",
|
||||
example: "9umDrYheeY451cQnEe",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -47,7 +47,7 @@ defmodule Pleroma.Web.ApiSpec.PollOperation do
|
|||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, FlakeID, "Poll ID",
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Poll ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
|
|||
default: false,
|
||||
description:
|
||||
"If the account is remote, should the report be forwarded to the remote admin?"
|
||||
},
|
||||
rule_ids: %Schema{
|
||||
type: :array,
|
||||
nullable: true,
|
||||
items: %Schema{type: :string},
|
||||
description: "Array of rules"
|
||||
}
|
||||
},
|
||||
required: [:account_id],
|
||||
|
|
@ -60,7 +66,8 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
|
|||
"account_id" => "123",
|
||||
"status_ids" => ["1337"],
|
||||
"comment" => "bad status!",
|
||||
"forward" => "false"
|
||||
"forward" => "false",
|
||||
"rule_ids" => ["3"]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
|
|||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, FlakeID, "Poll ID",
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Poll ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
|
|||
Operation.parameter(
|
||||
:account_id,
|
||||
:query,
|
||||
FlakeID,
|
||||
FlakeID.schema(),
|
||||
"If provided, statuses returned will be authored only by this account"
|
||||
),
|
||||
Operation.parameter(
|
||||
|
|
@ -116,7 +116,7 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
|
|||
Operation.parameter(
|
||||
:account_id,
|
||||
:query,
|
||||
FlakeID,
|
||||
FlakeID.schema(),
|
||||
"If provided, statuses returned will be authored only by this account"
|
||||
),
|
||||
Operation.parameter(
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike,
|
||||
BooleanLike.schema(),
|
||||
"Include reactions from muted acccounts."
|
||||
)
|
||||
],
|
||||
|
|
@ -82,7 +82,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike,
|
||||
BooleanLike.schema(),
|
||||
"Include reactions from muted acccounts."
|
||||
)
|
||||
],
|
||||
|
|
@ -256,6 +256,18 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
description: "Privately bookmark a status",
|
||||
operationId: "StatusController.bookmark",
|
||||
parameters: [id_param()],
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
title: "StatusUpdateRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
folder_id: %Schema{
|
||||
nullable: true,
|
||||
allOf: [FlakeID],
|
||||
description: "ID of bookmarks folder, if any"
|
||||
}
|
||||
}
|
||||
}),
|
||||
responses: %{
|
||||
200 => status_response()
|
||||
}
|
||||
|
|
@ -430,7 +442,15 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
summary: "Bookmarked statuses",
|
||||
description: "Statuses the user has bookmarked",
|
||||
operationId: "StatusController.bookmarks",
|
||||
parameters: pagination_params(),
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:folder_id,
|
||||
:query,
|
||||
FlakeID.schema(),
|
||||
"If provided, only display bookmarks from given folder"
|
||||
)
|
||||
| pagination_params()
|
||||
],
|
||||
security: [%{"oAuth" => ["read:bookmarks"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
|
||||
|
|
@ -534,7 +554,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
format: :"date-time",
|
||||
nullable: true,
|
||||
description:
|
||||
"ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
|
||||
"ISO 8601 Datetime at which to schedule a status. Providing this parameter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
|
||||
},
|
||||
language: %Schema{
|
||||
type: :string,
|
||||
|
|
@ -546,7 +566,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description:
|
||||
"If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
|
||||
"If set to `true` the post won't be actually posted, but the status entity would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
|
||||
},
|
||||
content_type: %Schema{
|
||||
type: :string,
|
||||
|
|
@ -581,6 +601,11 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
type: :string,
|
||||
description:
|
||||
"Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
|
||||
},
|
||||
quote_id: %Schema{
|
||||
nullable: true,
|
||||
allOf: [FlakeID],
|
||||
description: "ID of the status being quoted, if any"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
|
|
@ -680,7 +705,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
end
|
||||
|
||||
def id_param do
|
||||
Operation.parameter(:id, :path, FlakeID, "Status ID",
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Status ID",
|
||||
example: "9umDrYheeY451cQnEe",
|
||||
required: true
|
||||
)
|
||||
|
|
|
|||
464
lib/pleroma/web/api_spec/operations/streaming_operation.ex
Normal file
464
lib/pleroma/web/api_spec/operations/streaming_operation.ex
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
# 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.StreamingOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Response
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.NotificationOperation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Chat
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Conversation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
@spec open_api_operation(atom) :: Operation.t()
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
@spec streaming_operation() :: Operation.t()
|
||||
def streaming_operation do
|
||||
%Operation{
|
||||
tags: ["Timelines"],
|
||||
summary: "Establish streaming connection",
|
||||
description: """
|
||||
Receive statuses in real-time via WebSocket.
|
||||
|
||||
You can specify the access token on the query string or through the `sec-websocket-protocol` header. Using
|
||||
the query string to authenticate is considered unsafe and should not be used unless you have to (e.g. to maintain
|
||||
your client's compatibility with Mastodon).
|
||||
|
||||
You may specify a stream on the query string. If you do so and you are connecting to a stream that requires logged-in users,
|
||||
you must specify the access token at the time of the connection (i.e. via query string or header).
|
||||
|
||||
Otherwise, you have the option to authenticate after you have established the connection through client-sent events.
|
||||
|
||||
The "Request body" section below describes what events clients can send through WebSocket, and the "Responses" section
|
||||
describes what events server will send through WebSocket.
|
||||
""",
|
||||
security: [%{"oAuth" => ["read:statuses", "read:notifications"]}],
|
||||
operationId: "WebsocketHandler.streaming",
|
||||
parameters:
|
||||
[
|
||||
Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header",
|
||||
required: true
|
||||
),
|
||||
Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header",
|
||||
required: true
|
||||
),
|
||||
Operation.parameter(
|
||||
:"sec-websocket-key",
|
||||
:header,
|
||||
%Schema{type: :string},
|
||||
"sec-websocket-key header",
|
||||
required: true
|
||||
),
|
||||
Operation.parameter(
|
||||
:"sec-websocket-version",
|
||||
:header,
|
||||
%Schema{type: :string},
|
||||
"sec-websocket-version header",
|
||||
required: true
|
||||
)
|
||||
] ++ stream_params() ++ access_token_params(),
|
||||
requestBody: request_body("Client-sent events", client_sent_events()),
|
||||
responses: %{
|
||||
101 => switching_protocols_response(),
|
||||
200 =>
|
||||
Operation.response(
|
||||
"Server-sent events",
|
||||
"application/json",
|
||||
server_sent_events()
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp stream_params do
|
||||
stream_specifier()
|
||||
|> Enum.map(fn {name, schema} ->
|
||||
Operation.parameter(name, :query, schema, get_schema(schema).description)
|
||||
end)
|
||||
end
|
||||
|
||||
defp access_token_params do
|
||||
[
|
||||
Operation.parameter(:access_token, :query, token(), token().description),
|
||||
Operation.parameter(:"sec-websocket-protocol", :header, token(), token().description)
|
||||
]
|
||||
end
|
||||
|
||||
defp switching_protocols_response do
|
||||
%Response{
|
||||
description: "Switching protocols",
|
||||
headers: %{
|
||||
"connection" => %OpenApiSpex.Header{required: true},
|
||||
"upgrade" => %OpenApiSpex.Header{required: true},
|
||||
"sec-websocket-accept" => %OpenApiSpex.Header{required: true}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp server_sent_events do
|
||||
%Schema{
|
||||
oneOf: [
|
||||
update_event(),
|
||||
status_update_event(),
|
||||
notification_event(),
|
||||
chat_update_event(),
|
||||
follow_relationships_update_event(),
|
||||
conversation_event(),
|
||||
delete_event(),
|
||||
pleroma_respond_event()
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp stream do
|
||||
%Schema{
|
||||
type: :array,
|
||||
title: "Stream",
|
||||
description: """
|
||||
The stream identifier.
|
||||
The first item is the name of the stream. If the stream needs a differentiator, the second item will be the corresponding identifier.
|
||||
Currently, for the following stream types, there is a second element in the array:
|
||||
|
||||
- `list`: The second element is the id of the list, as a string.
|
||||
- `hashtag`: The second element is the name of the hashtag.
|
||||
- `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance.
|
||||
""",
|
||||
maxItems: 2,
|
||||
minItems: 1,
|
||||
items: %Schema{type: :string},
|
||||
example: ["hashtag", "mew"]
|
||||
}
|
||||
end
|
||||
|
||||
defp get_schema(%Schema{} = schema), do: schema
|
||||
defp get_schema(schema), do: schema.schema
|
||||
|
||||
defp server_sent_event_helper(name, description, type, payload, opts \\ []) do
|
||||
payload_type = Keyword.get(opts, :payload_type, :json)
|
||||
has_stream = Keyword.get(opts, :has_stream, true)
|
||||
|
||||
stream_properties =
|
||||
if has_stream do
|
||||
%{stream: stream()}
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
||||
stream_example = if has_stream, do: %{"stream" => get_schema(stream()).example}, else: %{}
|
||||
|
||||
stream_required = if has_stream, do: [:stream], else: []
|
||||
|
||||
payload_schema =
|
||||
if payload_type == :json do
|
||||
%Schema{
|
||||
title: "Event payload",
|
||||
description: "JSON-encoded string of #{get_schema(payload).title}",
|
||||
allOf: [payload]
|
||||
}
|
||||
else
|
||||
payload
|
||||
end
|
||||
|
||||
payload_example =
|
||||
if payload_type == :json do
|
||||
get_schema(payload).example |> Jason.encode!()
|
||||
else
|
||||
get_schema(payload).example
|
||||
end
|
||||
|
||||
%Schema{
|
||||
type: :object,
|
||||
title: name,
|
||||
description: description,
|
||||
required: [:event, :payload] ++ stream_required,
|
||||
properties:
|
||||
%{
|
||||
event: %Schema{
|
||||
title: "Event type",
|
||||
description: "Type of the event.",
|
||||
type: :string,
|
||||
required: true,
|
||||
enum: [type]
|
||||
},
|
||||
payload: payload_schema
|
||||
}
|
||||
|> Map.merge(stream_properties),
|
||||
example:
|
||||
%{
|
||||
"event" => type,
|
||||
"payload" => payload_example
|
||||
}
|
||||
|> Map.merge(stream_example)
|
||||
}
|
||||
end
|
||||
|
||||
defp update_event do
|
||||
server_sent_event_helper("New status", "A newly-posted status.", "update", Status)
|
||||
end
|
||||
|
||||
defp status_update_event do
|
||||
server_sent_event_helper("Edit", "A status that was just edited", "status.update", Status)
|
||||
end
|
||||
|
||||
defp notification_event do
|
||||
server_sent_event_helper(
|
||||
"Notification",
|
||||
"A new notification.",
|
||||
"notification",
|
||||
NotificationOperation.notification()
|
||||
)
|
||||
end
|
||||
|
||||
defp follow_relationships_update_event do
|
||||
server_sent_event_helper(
|
||||
"Follow relationships update",
|
||||
"An update to follow relationships.",
|
||||
"pleroma:follow_relationships_update",
|
||||
%Schema{
|
||||
type: :object,
|
||||
title: "Follow relationships update",
|
||||
required: [:state, :follower, :following],
|
||||
properties: %{
|
||||
state: %Schema{
|
||||
type: :string,
|
||||
description: "Follow state of the relationship.",
|
||||
enum: ["follow_pending", "follow_accept", "follow_reject", "unfollow"]
|
||||
},
|
||||
follower: %Schema{
|
||||
type: :object,
|
||||
description: "Information about the follower.",
|
||||
required: [:id, :follower_count, :following_count],
|
||||
properties: %{
|
||||
id: FlakeID,
|
||||
follower_count: %Schema{type: :integer},
|
||||
following_count: %Schema{type: :integer}
|
||||
}
|
||||
},
|
||||
following: %Schema{
|
||||
type: :object,
|
||||
description: "Information about the following person.",
|
||||
required: [:id, :follower_count, :following_count],
|
||||
properties: %{
|
||||
id: FlakeID,
|
||||
follower_count: %Schema{type: :integer},
|
||||
following_count: %Schema{type: :integer}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"state" => "follow_pending",
|
||||
"follower" => %{
|
||||
"id" => "someUser1",
|
||||
"follower_count" => 1,
|
||||
"following_count" => 1
|
||||
},
|
||||
"following" => %{
|
||||
"id" => "someUser2",
|
||||
"follower_count" => 1,
|
||||
"following_count" => 1
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp chat_update_event do
|
||||
server_sent_event_helper(
|
||||
"Chat update",
|
||||
"A new chat message.",
|
||||
"pleroma:chat_update",
|
||||
Chat
|
||||
)
|
||||
end
|
||||
|
||||
defp conversation_event do
|
||||
server_sent_event_helper(
|
||||
"Conversation update",
|
||||
"An update about a conversation",
|
||||
"conversation",
|
||||
Conversation
|
||||
)
|
||||
end
|
||||
|
||||
defp delete_event do
|
||||
server_sent_event_helper(
|
||||
"Delete",
|
||||
"A status that was just deleted.",
|
||||
"delete",
|
||||
%Schema{
|
||||
type: :string,
|
||||
title: "Status id",
|
||||
description: "Id of the deleted status",
|
||||
allOf: [FlakeID],
|
||||
example: "some-opaque-id"
|
||||
},
|
||||
payload_type: :string,
|
||||
has_stream: false
|
||||
)
|
||||
end
|
||||
|
||||
defp pleroma_respond_event do
|
||||
server_sent_event_helper(
|
||||
"Server response",
|
||||
"A response to a client-sent event.",
|
||||
"pleroma:respond",
|
||||
%Schema{
|
||||
type: :object,
|
||||
title: "Results",
|
||||
required: [:result, :type],
|
||||
properties: %{
|
||||
result: %Schema{
|
||||
type: :string,
|
||||
title: "Result of the request",
|
||||
enum: ["success", "error", "ignored"]
|
||||
},
|
||||
error: %Schema{
|
||||
type: :string,
|
||||
title: "Error code",
|
||||
description: "An error identifier. Only appears if `result` is `error`."
|
||||
},
|
||||
type: %Schema{
|
||||
type: :string,
|
||||
description: "Type of the request."
|
||||
}
|
||||
},
|
||||
example: %{"result" => "success", "type" => "pleroma:authenticate"}
|
||||
},
|
||||
has_stream: false
|
||||
)
|
||||
end
|
||||
|
||||
defp client_sent_events do
|
||||
%Schema{
|
||||
oneOf: [
|
||||
subscribe_event(),
|
||||
unsubscribe_event(),
|
||||
authenticate_event()
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp request_body(description, schema, opts \\ []) do
|
||||
%OpenApiSpex.RequestBody{
|
||||
description: description,
|
||||
content: %{
|
||||
"application/json" => %OpenApiSpex.MediaType{
|
||||
schema: schema,
|
||||
example: opts[:example],
|
||||
examples: opts[:examples]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp client_sent_event_helper(name, description, type, properties, opts) do
|
||||
required = opts[:required] || []
|
||||
|
||||
%Schema{
|
||||
type: :object,
|
||||
title: name,
|
||||
required: [:type] ++ required,
|
||||
description: description,
|
||||
properties:
|
||||
%{
|
||||
type: %Schema{type: :string, enum: [type], description: "Type of the event."}
|
||||
}
|
||||
|> Map.merge(properties),
|
||||
example: opts[:example]
|
||||
}
|
||||
end
|
||||
|
||||
defp subscribe_event do
|
||||
client_sent_event_helper(
|
||||
"Subscribe",
|
||||
"Subscribe to a stream.",
|
||||
"subscribe",
|
||||
stream_specifier(),
|
||||
required: [:stream],
|
||||
example: %{"type" => "subscribe", "stream" => "list", "list" => "1"}
|
||||
)
|
||||
end
|
||||
|
||||
defp unsubscribe_event do
|
||||
client_sent_event_helper(
|
||||
"Unsubscribe",
|
||||
"Unsubscribe from a stream.",
|
||||
"unsubscribe",
|
||||
stream_specifier(),
|
||||
required: [:stream],
|
||||
example: %{
|
||||
"type" => "unsubscribe",
|
||||
"stream" => "public:remote:media",
|
||||
"instance" => "example.org"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp authenticate_event do
|
||||
client_sent_event_helper(
|
||||
"Authenticate",
|
||||
"Authenticate via an access token.",
|
||||
"pleroma:authenticate",
|
||||
%{
|
||||
token: token()
|
||||
},
|
||||
required: [:token]
|
||||
)
|
||||
end
|
||||
|
||||
defp token do
|
||||
%Schema{
|
||||
type: :string,
|
||||
description: "An OAuth access token with corresponding permissions.",
|
||||
example: "some token"
|
||||
}
|
||||
end
|
||||
|
||||
defp stream_specifier do
|
||||
%{
|
||||
stream: %Schema{
|
||||
type: :string,
|
||||
description: "The name of the stream.",
|
||||
enum:
|
||||
Pleroma.Constants.public_streams() ++
|
||||
[
|
||||
"public:remote",
|
||||
"public:remote:media",
|
||||
"user",
|
||||
"user:pleroma_chat",
|
||||
"user:notification",
|
||||
"direct",
|
||||
"list",
|
||||
"hashtag"
|
||||
]
|
||||
},
|
||||
list: %Schema{
|
||||
type: :string,
|
||||
title: "List id",
|
||||
description: "The id of the list. Required when `stream` is `list`.",
|
||||
example: "some-id"
|
||||
},
|
||||
tag: %Schema{
|
||||
type: :string,
|
||||
title: "Hashtag name",
|
||||
description: "The name of the hashtag. Required when `stream` is `hashtag`.",
|
||||
example: "mew"
|
||||
},
|
||||
instance: %Schema{
|
||||
type: :string,
|
||||
title: "Domain name",
|
||||
description:
|
||||
"Domain name of the instance. Required when `stream` is `public:remote` or `public:remote:media`.",
|
||||
example: "example.org"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -176,7 +176,12 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
|
|||
end
|
||||
|
||||
defp with_muted_param do
|
||||
Operation.parameter(:with_muted, :query, BooleanLike, "Include activities by muted users")
|
||||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike.schema(),
|
||||
"Include activities by muted users"
|
||||
)
|
||||
end
|
||||
|
||||
defp exclude_visibilities_param do
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
defp change_password_request do
|
||||
%Schema{
|
||||
title: "ChangePasswordRequest",
|
||||
description: "POST body for changing the account's passowrd",
|
||||
description: "POST body for changing the account's password",
|
||||
type: :object,
|
||||
required: [:password, :new_password, :new_password_confirmation],
|
||||
properties: %{
|
||||
|
|
@ -136,23 +136,23 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def update_notificaton_settings_operation do
|
||||
def update_notification_settings_operation do
|
||||
%Operation{
|
||||
tags: ["Settings"],
|
||||
summary: "Update Notification Settings",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.update_notificaton_settings",
|
||||
operationId: "UtilController.update_notification_settings",
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:block_from_strangers,
|
||||
:query,
|
||||
BooleanLike,
|
||||
BooleanLike.schema(),
|
||||
"blocks notifications from accounts you do not follow"
|
||||
),
|
||||
Operation.parameter(
|
||||
:hide_notification_contents,
|
||||
:query,
|
||||
BooleanLike,
|
||||
BooleanLike.schema(),
|
||||
"removes the contents of a message from the push notification"
|
||||
)
|
||||
],
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
|
|||
title: "Attachment",
|
||||
description: "Represents a file or media attachment that can be added to a status.",
|
||||
type: :object,
|
||||
requried: [:id, :url, :preview_url],
|
||||
required: [:id, :url, :preview_url],
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "The ID of the attachment in the database."},
|
||||
url: %Schema{
|
||||
|
|
|
|||
26
lib/pleroma/web/api_spec/schemas/bookmark_folder.ex
Normal file
26
lib/pleroma/web/api_spec/schemas/bookmark_folder.ex
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# 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.Schemas.BookmarkFolder do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "BookmarkFolder",
|
||||
description: "Response schema for a bookmark folder",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: FlakeID,
|
||||
name: %Schema{type: :string, description: "Folder name"},
|
||||
emoji: %Schema{type: :string, description: "Folder emoji", nullable: true}
|
||||
},
|
||||
example: %{
|
||||
"id" => "9toJCu5YZW7O7gfvH6",
|
||||
"name" => "Read later",
|
||||
"emoji" => nil
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
@ -56,6 +56,15 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
|
|||
}
|
||||
},
|
||||
description: "Possible answers for the poll."
|
||||
},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
non_anonymous: %Schema{
|
||||
type: :boolean,
|
||||
description: "Can voters be publicly identified?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
|
|
@ -79,7 +88,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
|
|||
votes_count: 4
|
||||
}
|
||||
],
|
||||
emojis: []
|
||||
emojis: [],
|
||||
pleroma: %{
|
||||
non_anonymous: false
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
format: :uri,
|
||||
description: "Preview thumbnail"
|
||||
},
|
||||
image_description: %Schema{
|
||||
type: :string,
|
||||
description: "Alternate text that describes what is in the thumbnail"
|
||||
},
|
||||
title: %Schema{type: :string, description: "Title of linked resource"},
|
||||
description: %Schema{type: :string, description: "Description of preview"}
|
||||
}
|
||||
|
|
@ -193,6 +197,30 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
nullable: true,
|
||||
description: "The `acct` property of User entity for replied user (if any)"
|
||||
},
|
||||
quote: %Schema{
|
||||
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
|
||||
nullable: true,
|
||||
description: "Quoted status (if any)"
|
||||
},
|
||||
quote_id: %Schema{
|
||||
nullable: true,
|
||||
allOf: [FlakeID],
|
||||
description: "ID of the status being quoted, if any"
|
||||
},
|
||||
quote_url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
nullable: true,
|
||||
description: "URL of the quoted status"
|
||||
},
|
||||
quote_visible: %Schema{
|
||||
type: :boolean,
|
||||
description: "`true` if the quoted post is visible to the user"
|
||||
},
|
||||
quotes_count: %Schema{
|
||||
type: :integer,
|
||||
description: "How many statuses quoted this status"
|
||||
},
|
||||
local: %Schema{
|
||||
type: :boolean,
|
||||
description: "`true` if the post was made on the local instance"
|
||||
|
|
@ -347,7 +375,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
"in_reply_to_account_acct" => nil,
|
||||
"local" => true,
|
||||
"spoiler_text" => %{"text/plain" => ""},
|
||||
"thread_muted" => false
|
||||
"thread_muted" => false,
|
||||
"quotes_count" => 0
|
||||
},
|
||||
"poll" => nil,
|
||||
"reblog" => nil,
|
||||
|
|
|
|||
82
lib/pleroma/web/api_spec/scopes/compiler.ex
Normal file
82
lib/pleroma/web/api_spec/scopes/compiler.ex
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Scopes.Compiler do
|
||||
defmacro __before_compile__(_env) do
|
||||
strings = __MODULE__.extract_all_scopes()
|
||||
|
||||
quote do
|
||||
def placeholder do
|
||||
unquote do
|
||||
Enum.map(
|
||||
strings,
|
||||
fn string ->
|
||||
quote do
|
||||
Pleroma.Web.Gettext.dgettext_noop(
|
||||
"oauth_scopes",
|
||||
unquote(string)
|
||||
)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def extract_all_scopes do
|
||||
extract_all_scopes_from(Pleroma.Web.ApiSpec.spec())
|
||||
end
|
||||
|
||||
def extract_all_scopes_from(specs) do
|
||||
specs.paths
|
||||
|> Enum.reduce([], fn
|
||||
{_path, %{} = path_item}, acc ->
|
||||
extract_routes(path_item)
|
||||
|> Enum.flat_map(fn operation -> process_operation(operation) end)
|
||||
|> Kernel.++(acc)
|
||||
|
||||
{_, _}, acc ->
|
||||
acc
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp extract_routes(path_item) do
|
||||
path_item
|
||||
|> Map.from_struct()
|
||||
|> Enum.map(fn {_method, path_item} -> path_item end)
|
||||
|> Enum.filter(fn
|
||||
%OpenApiSpex.Operation{} = _operation -> true
|
||||
_ -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp process_operation(operation) do
|
||||
operation.security
|
||||
|> Kernel.||([])
|
||||
|> Enum.flat_map(fn
|
||||
%{"oAuth" => scopes} -> process_scopes(scopes)
|
||||
_ -> []
|
||||
end)
|
||||
end
|
||||
|
||||
defp process_scopes(scopes) do
|
||||
scopes
|
||||
|> Enum.flat_map(fn scope ->
|
||||
process_scope(scope)
|
||||
end)
|
||||
end
|
||||
|
||||
def process_scope(scope) do
|
||||
hierarchy = String.split(scope, ":")
|
||||
|
||||
{_, list} =
|
||||
Enum.reduce(hierarchy, {"", []}, fn comp, {cur, list} ->
|
||||
{cur <> comp <> ":", [cur <> comp | list]}
|
||||
end)
|
||||
|
||||
list
|
||||
end
|
||||
end
|
||||
10
lib/pleroma/web/api_spec/scopes/translator.ex
Normal file
10
lib/pleroma/web/api_spec/scopes/translator.ex
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Scopes.Translator do
|
||||
require Pleroma.Web.ApiSpec.Scopes.Compiler
|
||||
require Pleroma.Web.Gettext
|
||||
|
||||
@before_compile Pleroma.Web.ApiSpec.Scopes.Compiler
|
||||
end
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
defmodule Pleroma.Web.Auth.Authenticator do
|
||||
@callback get_user(Plug.Conn.t()) :: {:ok, user :: struct()} | {:error, any()}
|
||||
@callback create_from_registration(Plug.Conn.t(), registration :: struct()) ::
|
||||
{:ok, User.t()} | {:error, any()}
|
||||
{:ok, Pleroma.User.t()} | {:error, any()}
|
||||
@callback get_registration(Plug.Conn.t()) :: {:ok, registration :: struct()} | {:error, any()}
|
||||
@callback handle_error(Plug.Conn.t(), any()) :: any()
|
||||
@callback auth_template() :: String.t() | nil
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
alias Pleroma.Formatter
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Rule
|
||||
alias Pleroma.ThreadMute
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
|
|
@ -33,6 +34,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
|
||||
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
|
||||
:ok <- validate_chat_attachment_attribution(maybe_attachment, user),
|
||||
:ok <- validate_chat_content_length(content, !!maybe_attachment),
|
||||
{_, {:ok, chat_message_data, _meta}} <-
|
||||
{:build_object,
|
||||
|
|
@ -71,6 +73,17 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
text
|
||||
end
|
||||
|
||||
defp validate_chat_attachment_attribution(nil, _), do: :ok
|
||||
|
||||
defp validate_chat_attachment_attribution(attachment, user) do
|
||||
with :ok <- Object.authorize_access(attachment, user) do
|
||||
:ok
|
||||
else
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_chat_content_length(_, true), do: :ok
|
||||
defp validate_chat_content_length(nil, false), do: {:error, :no_content}
|
||||
|
||||
|
|
@ -142,7 +155,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def delete(activity_id, user) do
|
||||
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
|
||||
{:find_activity, Activity.get_by_id(activity_id)},
|
||||
{:find_activity, Activity.get_by_id(activity_id, filter: [])},
|
||||
{_, %Object{} = object, _} <-
|
||||
{:find_object, Object.normalize(activity, fetch: false), activity},
|
||||
true <- User.privileged?(user, :messages_delete) || user.ap_id == object.data["actor"],
|
||||
|
|
@ -360,7 +373,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
do: visibility in ~w(public unlisted)
|
||||
|
||||
def public_announce?(object, _) do
|
||||
Visibility.is_public?(object)
|
||||
Visibility.public?(object)
|
||||
end
|
||||
|
||||
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
|
||||
|
|
@ -488,12 +501,12 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
|
||||
defp activity_is_public(activity) do
|
||||
with false <- Visibility.is_public?(activity) do
|
||||
with false <- Visibility.public?(activity) do
|
||||
{:error, :visibility_error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
|
||||
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
|
||||
def unpin(id, user) do
|
||||
with %Activity{} = activity <- create_activity_by_id(id),
|
||||
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
|
||||
|
|
@ -538,7 +551,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
remove_mute(user, activity)
|
||||
else
|
||||
{what, result} = error ->
|
||||
Logger.warn(
|
||||
Logger.warning(
|
||||
"CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
|
||||
)
|
||||
|
||||
|
|
@ -556,14 +569,16 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
def report(user, data) 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) do
|
||||
{:ok, statuses} <- get_report_statuses(account, data),
|
||||
rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do
|
||||
ActivityPub.flag(%{
|
||||
context: Utils.generate_context_id(),
|
||||
actor: user,
|
||||
account: account,
|
||||
statuses: statuses,
|
||||
content: content_html,
|
||||
forward: Map.get(data, :forward, false)
|
||||
forward: Map.get(data, :forward, false),
|
||||
rules: rules
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
@ -575,6 +590,15 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
defp get_report_rules(nil) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp get_report_rules(rule_ids) do
|
||||
rule_ids
|
||||
|> Enum.filter(&Rule.exists?/1)
|
||||
end
|
||||
|
||||
def update_report_state(activity_ids, state) when is_list(activity_ids) do
|
||||
case Utils.update_report_state(activity_ids, state) do
|
||||
:ok -> {:ok, activity_ids}
|
||||
|
|
@ -583,7 +607,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
|
||||
def update_report_state(activity_id, state) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(activity_id, filter: []) do
|
||||
Utils.update_report_state(activity, state)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
import Pleroma.Web.Gettext
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
defstruct valid?: true,
|
||||
errors: [],
|
||||
|
|
@ -22,6 +26,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
attachments: [],
|
||||
in_reply_to: nil,
|
||||
in_reply_to_conversation: nil,
|
||||
quote_post: nil,
|
||||
visibility: nil,
|
||||
expires_at: nil,
|
||||
extra: nil,
|
||||
|
|
@ -53,7 +58,9 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|> poll()
|
||||
|> with_valid(&in_reply_to/1)
|
||||
|> with_valid(&in_reply_to_conversation/1)
|
||||
|> with_valid("e_post/1)
|
||||
|> with_valid(&visibility/1)
|
||||
|> with_valid("ing_visibility/1)
|
||||
|> content()
|
||||
|> with_valid(&to_and_cc/1)
|
||||
|> with_valid(&context/1)
|
||||
|
|
@ -78,7 +85,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
defp listen_object(draft) do
|
||||
object =
|
||||
draft.params
|
||||
|> Map.take([:album, :artist, :title, :length])
|
||||
|> Map.take([:album, :artist, :title, :length, :externalLink])
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("type", "Audio")
|
||||
|> Map.put("to", draft.to)
|
||||
|
|
@ -111,7 +118,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
end
|
||||
|
||||
defp attachments(%{params: params} = draft) do
|
||||
attachments = Utils.attachments_from_ids(params)
|
||||
attachments = Utils.attachments_from_ids(params, draft.user)
|
||||
draft = %__MODULE__{draft | attachments: attachments}
|
||||
|
||||
case Utils.validate_attachments_count(attachments) do
|
||||
|
|
@ -122,8 +129,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
|
||||
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
|
||||
%__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: :deleted}} = draft) do
|
||||
add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
|
||||
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
|
||||
|
|
@ -132,6 +153,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|
||||
defp in_reply_to(draft), do: draft
|
||||
|
||||
defp quote_post(%{params: %{quote_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
|
||||
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}
|
||||
|
|
@ -147,6 +180,29 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
end
|
||||
end
|
||||
|
||||
defp can_quote?(_draft, _object, visibility) when visibility in ~w(public unlisted local) do
|
||||
true
|
||||
end
|
||||
|
||||
defp can_quote?(draft, object, "private") do
|
||||
draft.user.ap_id == object.data["actor"]
|
||||
end
|
||||
|
||||
defp can_quote?(_, _, _) do
|
||||
false
|
||||
end
|
||||
|
||||
defp quoting_visibility(%{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
|
||||
else
|
||||
_ -> add_error(draft, dgettext("errors", "Cannot quote private message"))
|
||||
end
|
||||
end
|
||||
|
||||
defp quoting_visibility(draft), do: draft
|
||||
|
||||
defp expires_at(draft) do
|
||||
case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
|
||||
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
|
||||
|
|
@ -164,12 +220,15 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
end
|
||||
end
|
||||
|
||||
defp content(draft) do
|
||||
defp content(%{mentions: mentions} = draft) do
|
||||
{content_html, mentioned_users, tags} = Utils.make_content_html(draft)
|
||||
|
||||
mentioned_ap_ids =
|
||||
Enum.map(mentioned_users, fn {_, mentioned_user} -> mentioned_user.ap_id end)
|
||||
|
||||
mentions =
|
||||
mentioned_users
|
||||
|> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
|
||||
mentions
|
||||
|> Kernel.++(mentioned_ap_ids)
|
||||
|> Utils.get_addressed_users(draft.params[:to])
|
||||
|
||||
%__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
|
||||
|
|
|
|||
|
|
@ -23,21 +23,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do
|
||||
attachments_from_ids_descs(ids, desc)
|
||||
def attachments_from_ids(%{media_ids: ids, descriptions: desc}, user) do
|
||||
attachments_from_ids_descs(ids, desc, user)
|
||||
end
|
||||
|
||||
def attachments_from_ids(%{media_ids: ids}) do
|
||||
attachments_from_ids_no_descs(ids)
|
||||
def attachments_from_ids(%{media_ids: ids}, user) do
|
||||
attachments_from_ids_no_descs(ids, user)
|
||||
end
|
||||
|
||||
def attachments_from_ids(_), do: []
|
||||
def attachments_from_ids(_, _), do: []
|
||||
|
||||
def attachments_from_ids_no_descs([]), do: []
|
||||
def attachments_from_ids_no_descs([], _), do: []
|
||||
|
||||
def attachments_from_ids_no_descs(ids) do
|
||||
def attachments_from_ids_no_descs(ids, user) do
|
||||
Enum.map(ids, fn media_id ->
|
||||
case get_attachment(media_id) do
|
||||
case get_attachment(media_id, user) do
|
||||
%Object{data: data} -> data
|
||||
_ -> nil
|
||||
end
|
||||
|
|
@ -45,21 +45,27 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
def attachments_from_ids_descs([], _), do: []
|
||||
def attachments_from_ids_descs([], _, _), do: []
|
||||
|
||||
def attachments_from_ids_descs(ids, descs_str) do
|
||||
def attachments_from_ids_descs(ids, descs_str, user) do
|
||||
{_, descs} = Jason.decode(descs_str)
|
||||
|
||||
Enum.map(ids, fn media_id ->
|
||||
with %Object{data: data} <- get_attachment(media_id) do
|
||||
with %Object{data: data} <- get_attachment(media_id, user) do
|
||||
Map.put(data, "name", descs[media_id])
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp get_attachment(media_id) do
|
||||
Repo.get(Object, media_id)
|
||||
defp get_attachment(media_id, user) do
|
||||
with %Object{data: data} = object <- Repo.get(Object, media_id),
|
||||
%{"type" => type} when type in Pleroma.Constants.upload_object_types() <- data,
|
||||
:ok <- Object.authorize_access(object, user) do
|
||||
object
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
|
||||
|
|
@ -103,7 +109,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|
||||
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.is_direct?(draft.in_reply_to) do
|
||||
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, []}
|
||||
|
|
@ -145,6 +151,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
when is_list(options) do
|
||||
limits = Config.get([:instance, :poll_limits])
|
||||
|
||||
options = options |> Enum.uniq()
|
||||
|
||||
with :ok <- validate_poll_expiration(expires_in, limits),
|
||||
:ok <- validate_poll_options_amount(options, limits),
|
||||
:ok <- validate_poll_options_length(options, limits) do
|
||||
|
|
@ -180,10 +188,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
end
|
||||
|
||||
defp validate_poll_options_amount(options, %{max_options: max_options}) do
|
||||
if Enum.count(options) > max_options do
|
||||
{:error, "Poll can't contain more than #{max_options} options"}
|
||||
else
|
||||
:ok
|
||||
cond do
|
||||
Enum.count(options) < 2 ->
|
||||
{:error, "Poll must contain at least 2 options"}
|
||||
|
||||
Enum.count(options) > max_options ->
|
||||
{:error, "Poll can't contain more than #{max_options} options"}
|
||||
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -308,13 +321,13 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
format_asctime(date)
|
||||
else
|
||||
_e ->
|
||||
Logger.warn("Date #{date} in wrong format, must be ISO 8601")
|
||||
Logger.warning("Date #{date} in wrong format, must be ISO 8601")
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
def date_to_asctime(date) do
|
||||
Logger.warn("Date #{date} in wrong format, must be ISO 8601")
|
||||
Logger.warning("Date #{date} in wrong format, must be ISO 8601")
|
||||
""
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ defmodule Pleroma.Web.ControllerHelper do
|
|||
|> json(json)
|
||||
end
|
||||
|
||||
@spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil
|
||||
@spec fetch_integer_param(map(), String.t() | atom(), integer() | nil) :: integer() | nil
|
||||
def fetch_integer_param(params, name, default \\ nil) do
|
||||
params
|
||||
|> Map.get(name, default)
|
||||
|
|
@ -53,10 +53,15 @@ defmodule Pleroma.Web.ControllerHelper do
|
|||
end
|
||||
end
|
||||
|
||||
# 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
|
||||
params =
|
||||
conn.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)])
|
||||
else
|
||||
conn.params
|
||||
end
|
||||
|> Map.drop(Map.keys(conn.path_params) |> Enum.map(&String.to_existing_atom/1))
|
||||
|> Map.merge(extra_params)
|
||||
|> Map.drop(@id_keys)
|
||||
|
|
@ -85,18 +90,15 @@ defmodule Pleroma.Web.ControllerHelper do
|
|||
end
|
||||
end
|
||||
|
||||
def assign_account_by_id(conn, _) do
|
||||
case Pleroma.User.get_cached_by_id(conn.params.id) do
|
||||
def assign_account_by_id(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
|
||||
case Pleroma.User.get_cached_by_id(id) do
|
||||
%Pleroma.User{} = account -> assign(conn, :account, account)
|
||||
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
|
||||
end
|
||||
end
|
||||
|
||||
def try_render(conn, target, params) when is_binary(target) do
|
||||
case render(conn, target, params) do
|
||||
nil -> render_error(conn, :not_implemented, "Can't display this activity")
|
||||
res -> res
|
||||
end
|
||||
render(conn, target, params)
|
||||
end
|
||||
|
||||
def try_render(conn, _, _) do
|
||||
|
|
|
|||
|
|
@ -11,12 +11,10 @@ defmodule Pleroma.Web.EmbedController do
|
|||
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
||||
plug(:put_layout, :embed)
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
with %Activity{local: true} = activity <-
|
||||
Activity.get_by_id_with_object(id),
|
||||
true <- Visibility.is_public?(activity.object) do
|
||||
true <- Visibility.public?(activity.object) do
|
||||
{:ok, author} = User.get_or_fetch(activity.object.data["actor"])
|
||||
|
||||
conn
|
||||
|
|
|
|||
|
|
@ -9,7 +9,31 @@ defmodule Pleroma.Web.Endpoint do
|
|||
|
||||
alias Pleroma.Config
|
||||
|
||||
socket("/socket", Pleroma.Web.UserSocket)
|
||||
socket("/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler,
|
||||
longpoll: false,
|
||||
websocket: [
|
||||
path: "/",
|
||||
compress: false,
|
||||
error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []},
|
||||
fullsweep_after: 20
|
||||
]
|
||||
)
|
||||
|
||||
socket("/socket", Pleroma.Web.UserSocket,
|
||||
websocket: [
|
||||
path: "/websocket",
|
||||
serializer: [
|
||||
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
|
||||
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
|
||||
],
|
||||
timeout: 60_000,
|
||||
transport_log: false,
|
||||
compress: false,
|
||||
fullsweep_after: 20
|
||||
],
|
||||
longpoll: false
|
||||
)
|
||||
|
||||
socket("/live", Phoenix.LiveView.Socket)
|
||||
|
||||
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
|
||||
|
|
@ -19,7 +43,8 @@ defmodule Pleroma.Web.Endpoint do
|
|||
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
|
||||
plug(Pleroma.Web.Plugs.UploadedMedia)
|
||||
|
||||
@static_cache_control "public, no-cache"
|
||||
@static_cache_control "public, max-age=1209600"
|
||||
@static_cache_disabled "public, no-cache"
|
||||
|
||||
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
|
||||
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
|
||||
|
|
@ -30,22 +55,32 @@ defmodule Pleroma.Web.Endpoint do
|
|||
from: :pleroma,
|
||||
only: ["emoji", "images"],
|
||||
gzip: true,
|
||||
cache_control_for_etags: "public, max-age=1209600",
|
||||
headers: %{
|
||||
"cache-control" => "public, max-age=1209600"
|
||||
}
|
||||
)
|
||||
|
||||
plug(Pleroma.Web.Plugs.InstanceStatic,
|
||||
at: "/",
|
||||
gzip: true,
|
||||
cache_control_for_etags: @static_cache_control,
|
||||
headers: %{
|
||||
"cache-control" => @static_cache_control
|
||||
}
|
||||
)
|
||||
|
||||
# Careful! No `only` restriction here, as we don't know what frontends contain.
|
||||
plug(Pleroma.Web.Plugs.InstanceStatic,
|
||||
at: "/",
|
||||
gzip: true,
|
||||
cache_control_for_etags: @static_cache_disabled,
|
||||
headers: %{
|
||||
"cache-control" => @static_cache_disabled
|
||||
}
|
||||
)
|
||||
|
||||
plug(Pleroma.Web.Plugs.FrontendStatic,
|
||||
at: "/",
|
||||
frontend_type: :primary,
|
||||
only: ["index.html"],
|
||||
gzip: true,
|
||||
cache_control_for_etags: @static_cache_disabled,
|
||||
headers: %{
|
||||
"cache-control" => @static_cache_disabled
|
||||
}
|
||||
)
|
||||
|
||||
plug(Pleroma.Web.Plugs.FrontendStatic,
|
||||
at: "/",
|
||||
frontend_type: :primary,
|
||||
|
|
@ -62,9 +97,9 @@ defmodule Pleroma.Web.Endpoint do
|
|||
at: "/pleroma/admin",
|
||||
frontend_type: :admin,
|
||||
gzip: true,
|
||||
cache_control_for_etags: @static_cache_control,
|
||||
cache_control_for_etags: @static_cache_disabled,
|
||||
headers: %{
|
||||
"cache-control" => @static_cache_control
|
||||
"cache-control" => @static_cache_disabled
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -79,9 +114,9 @@ defmodule Pleroma.Web.Endpoint do
|
|||
only: Pleroma.Constants.static_only_files(),
|
||||
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
||||
gzip: true,
|
||||
cache_control_for_etags: @static_cache_control,
|
||||
cache_control_for_etags: @static_cache_disabled,
|
||||
headers: %{
|
||||
"cache-control" => @static_cache_control
|
||||
"cache-control" => @static_cache_disabled
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -101,13 +136,10 @@ defmodule Pleroma.Web.Endpoint do
|
|||
plug(Plug.Logger, log: :debug)
|
||||
|
||||
plug(Plug.Parsers,
|
||||
parsers: [
|
||||
:urlencoded,
|
||||
{:multipart, length: {Config, :get, [[:instance, :upload_limit]]}},
|
||||
:json
|
||||
],
|
||||
parsers: [:urlencoded, Pleroma.Web.Multipart, :json],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Jason,
|
||||
# Note: this is compile-time only, won't work for database-config
|
||||
length: Config.get([:instance, :upload_limit]),
|
||||
body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
|
||||
)
|
||||
|
|
@ -141,47 +173,6 @@ defmodule Pleroma.Web.Endpoint do
|
|||
|
||||
plug(Pleroma.Web.Plugs.RemoteIp)
|
||||
|
||||
defmodule Instrumenter do
|
||||
use Prometheus.PhoenixInstrumenter
|
||||
end
|
||||
|
||||
defmodule PipelineInstrumenter do
|
||||
use Prometheus.PlugPipelineInstrumenter
|
||||
end
|
||||
|
||||
defmodule MetricsExporter do
|
||||
use Prometheus.PlugExporter
|
||||
end
|
||||
|
||||
defmodule MetricsExporterCaller do
|
||||
@behaviour Plug
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, opts) do
|
||||
prometheus_config = Application.get_env(:prometheus, MetricsExporter, [])
|
||||
ip_whitelist = List.wrap(prometheus_config[:ip_whitelist])
|
||||
|
||||
cond do
|
||||
!prometheus_config[:enabled] ->
|
||||
conn
|
||||
|
||||
ip_whitelist != [] and
|
||||
!Enum.find(ip_whitelist, fn ip ->
|
||||
Pleroma.Helpers.InetHelper.parse_address(ip) == {:ok, conn.remote_ip}
|
||||
end) ->
|
||||
conn
|
||||
|
||||
true ->
|
||||
MetricsExporter.call(conn, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
plug(PipelineInstrumenter)
|
||||
|
||||
plug(MetricsExporterCaller)
|
||||
|
||||
plug(Pleroma.Web.Router)
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -17,10 +17,28 @@ defmodule Pleroma.Web.Fallback.RedirectController do
|
|||
|> json(%{error: "Not implemented"})
|
||||
end
|
||||
|
||||
def add_generated_metadata(page_content, extra \\ "") do
|
||||
title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"
|
||||
favicon = "<link rel='icon' href='#{Pleroma.Config.get([:instance, :favicon])}'>"
|
||||
manifest = "<link rel='manifest' href='/manifest.json'>"
|
||||
|
||||
page_content
|
||||
|> String.replace(
|
||||
"<!--server-generated-meta-->",
|
||||
title <> favicon <> manifest <> extra
|
||||
)
|
||||
end
|
||||
|
||||
def redirector(conn, _params, code \\ 200) do
|
||||
{:ok, index_content} = File.read(index_file_path())
|
||||
|
||||
response =
|
||||
index_content
|
||||
|> add_generated_metadata()
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_file(code, index_file_path())
|
||||
|> send_resp(code, response)
|
||||
end
|
||||
|
||||
def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
|
||||
|
|
@ -34,14 +52,12 @@ defmodule Pleroma.Web.Fallback.RedirectController do
|
|||
|
||||
def redirector_with_meta(conn, params) do
|
||||
{:ok, index_content} = File.read(index_file_path())
|
||||
|
||||
tags = build_tags(conn, params)
|
||||
preloads = preload_data(conn, params)
|
||||
title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"
|
||||
|
||||
response =
|
||||
index_content
|
||||
|> String.replace("<!--server-generated-meta-->", tags <> preloads <> title)
|
||||
|> add_generated_metadata(tags <> preloads)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|
|
@ -55,11 +71,10 @@ defmodule Pleroma.Web.Fallback.RedirectController do
|
|||
def redirector_with_preload(conn, params) do
|
||||
{:ok, index_content} = File.read(index_file_path())
|
||||
preloads = preload_data(conn, params)
|
||||
title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"
|
||||
|
||||
response =
|
||||
index_content
|
||||
|> String.replace("<!--server-generated-meta-->", preloads <> title)
|
||||
|> add_generated_metadata(preloads)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|
|
|
|||
|
|
@ -6,10 +6,9 @@ defmodule Pleroma.Web.Federator do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Publisher
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Federator.Publisher
|
||||
alias Pleroma.Workers.PublisherWorker
|
||||
alias Pleroma.Workers.ReceiverWorker
|
||||
|
||||
|
|
@ -36,6 +35,17 @@ defmodule Pleroma.Web.Federator do
|
|||
end
|
||||
|
||||
# Client API
|
||||
def incoming_ap_doc(%{params: params, req_headers: req_headers}) do
|
||||
ReceiverWorker.enqueue(
|
||||
"incoming_ap_doc",
|
||||
%{"req_headers" => req_headers, "params" => params, "timeout" => :timer.seconds(20)},
|
||||
priority: 2
|
||||
)
|
||||
end
|
||||
|
||||
def incoming_ap_doc(%{"type" => "Delete"} = params) do
|
||||
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params}, priority: 3)
|
||||
end
|
||||
|
||||
def incoming_ap_doc(params) do
|
||||
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params})
|
||||
|
|
@ -58,10 +68,8 @@ defmodule Pleroma.Web.Federator do
|
|||
|
||||
# Job Worker Callbacks
|
||||
|
||||
@spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
|
||||
def perform(:publish_one, module, params) do
|
||||
apply(module, :publish_one, [params])
|
||||
end
|
||||
@spec perform(atom(), any()) :: {:ok, any()} | {:error, any()}
|
||||
def perform(:publish_one, params), do: Publisher.publish_one(params)
|
||||
|
||||
def perform(:publish, activity) do
|
||||
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
|
||||
|
|
@ -80,7 +88,7 @@ defmodule Pleroma.Web.Federator do
|
|||
|
||||
# NOTE: we use the actor ID to do the containment, this is fine because an
|
||||
# actor shouldn't be acting on objects outside their own AP server.
|
||||
with {_, {:ok, _user}} <- {:actor, ap_enabled_actor(actor)},
|
||||
with {_, {:ok, _user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)},
|
||||
nil <- Activity.normalize(params["id"]),
|
||||
{_, :ok} <-
|
||||
{:correct_origin?, Containment.contain_origin_from_id(actor, params)},
|
||||
|
|
@ -110,14 +118,4 @@ defmodule Pleroma.Web.Federator do
|
|||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def ap_enabled_actor(id) do
|
||||
user = User.get_cached_by_ap_id(id)
|
||||
|
||||
if User.ap_enabled?(user) do
|
||||
{:ok, user}
|
||||
else
|
||||
ActivityPub.make_user_from_ap_id(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,109 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Federator.Publisher do
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Workers.PublisherWorker
|
||||
|
||||
require Logger
|
||||
|
||||
@moduledoc """
|
||||
Defines the contract used by federation implementations to publish messages to
|
||||
their peers.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Determine whether an activity can be relayed using the federation module.
|
||||
"""
|
||||
@callback is_representable?(Pleroma.Activity.t()) :: boolean()
|
||||
|
||||
@doc """
|
||||
Relays an activity to a specified peer, determined by the parameters. The
|
||||
parameters used are controlled by the federation module.
|
||||
"""
|
||||
@callback publish_one(Map.t()) :: {:ok, Map.t()} | {:error, any()}
|
||||
|
||||
@doc """
|
||||
Enqueue publishing a single activity.
|
||||
"""
|
||||
@spec enqueue_one(module(), Map.t()) :: :ok
|
||||
def enqueue_one(module, %{} = params) do
|
||||
PublisherWorker.enqueue(
|
||||
"publish_one",
|
||||
%{"module" => to_string(module), "params" => params}
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Relays an activity to all specified peers.
|
||||
"""
|
||||
@callback publish(User.t(), Activity.t()) :: :ok | {:error, any()}
|
||||
|
||||
@spec publish(User.t(), Activity.t()) :: :ok
|
||||
def publish(%User{} = user, %Activity{} = activity) do
|
||||
Config.get([:instance, :federation_publisher_modules])
|
||||
|> Enum.each(fn module ->
|
||||
if module.is_representable?(activity) do
|
||||
Logger.debug("Publishing #{activity.data["id"]} using #{inspect(module)}")
|
||||
module.publish(user, activity)
|
||||
end
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gathers links used by an outgoing federation module for WebFinger output.
|
||||
"""
|
||||
@callback gather_webfinger_links(User.t()) :: list()
|
||||
|
||||
@spec gather_webfinger_links(User.t()) :: list()
|
||||
def gather_webfinger_links(%User{} = user) do
|
||||
Config.get([:instance, :federation_publisher_modules])
|
||||
|> Enum.reduce([], fn module, links ->
|
||||
links ++ module.gather_webfinger_links(user)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gathers nodeinfo protocol names supported by the federation module.
|
||||
"""
|
||||
@callback gather_nodeinfo_protocol_names() :: list()
|
||||
|
||||
@spec gather_nodeinfo_protocol_names() :: list()
|
||||
def gather_nodeinfo_protocol_names do
|
||||
Config.get([:instance, :federation_publisher_modules])
|
||||
|> Enum.reduce([], fn module, links ->
|
||||
links ++ module.gather_nodeinfo_protocol_names()
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gathers a set of remote users given an IR envelope.
|
||||
"""
|
||||
def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
|
||||
cc = Map.get(data, "cc", [])
|
||||
|
||||
bcc =
|
||||
data
|
||||
|> Map.get("bcc", [])
|
||||
|> Enum.reduce([], fn ap_id, bcc ->
|
||||
case Pleroma.List.get_by_ap_id(ap_id) do
|
||||
%Pleroma.List{user_id: ^user_id} = list ->
|
||||
{:ok, following} = Pleroma.List.get_following(list)
|
||||
bcc ++ Enum.map(following, & &1.ap_id)
|
||||
|
||||
_ ->
|
||||
bcc
|
||||
end
|
||||
end)
|
||||
|
||||
[to, cc, bcc]
|
||||
|> Enum.concat()
|
||||
|> Enum.map(&User.get_cached_by_ap_id/1)
|
||||
|> Enum.filter(fn user -> user && !user.local end)
|
||||
end
|
||||
end
|
||||
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
use Phoenix.HTML
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Gettext
|
||||
|
|
@ -72,7 +71,9 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
|
||||
def last_activity(activities), do: List.last(activities)
|
||||
|
||||
def activity_title(%{"content" => content, "summary" => summary} = data, opts \\ %{}) do
|
||||
def activity_title(%{"content" => content} = data, opts \\ %{}) do
|
||||
summary = Map.get(data, "summary", "")
|
||||
|
||||
title =
|
||||
cond do
|
||||
summary != "" -> summary
|
||||
|
|
@ -81,9 +82,8 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
end
|
||||
|
||||
title
|
||||
|> Pleroma.Web.Metadata.Utils.scrub_html()
|
||||
|> Pleroma.Emoji.Formatter.demojify()
|
||||
|> Formatter.truncate(opts[:max_length], opts[:omission])
|
||||
|> Pleroma.Web.Metadata.Utils.scrub_html_and_truncate(opts[:max_length], opts[:omission])
|
||||
|> HtmlEntities.encode()
|
||||
end
|
||||
|
||||
def activity_description(data) do
|
||||
|
|
@ -132,7 +132,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
|> safe_to_string()
|
||||
end
|
||||
|
||||
@spec to_rfc3339(String.t() | NativeDateTime.t()) :: String.t()
|
||||
@spec to_rfc3339(String.t() | NaiveDateTime.t()) :: String.t()
|
||||
def to_rfc3339(date) when is_binary(date) do
|
||||
date
|
||||
|> Timex.parse!("{ISO:Extended}")
|
||||
|
|
@ -145,7 +145,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
|> Timex.format!("{RFC3339}")
|
||||
end
|
||||
|
||||
@spec to_rfc2822(String.t() | DateTime.t() | NativeDateTime.t()) :: String.t()
|
||||
@spec to_rfc2822(String.t() | DateTime.t() | NaiveDateTime.t()) :: String.t()
|
||||
def to_rfc2822(datestr) when is_binary(datestr) do
|
||||
datestr
|
||||
|> Timex.parse!("{ISO:Extended}")
|
||||
|
|
|
|||
|
|
@ -85,12 +85,12 @@ defmodule Pleroma.Web.Gettext do
|
|||
Process.get({Pleroma.Web.Gettext, :locales}, [])
|
||||
end
|
||||
|
||||
def is_locale_list(locales) do
|
||||
def locale_list?(locales) do
|
||||
Enum.all?(locales, &is_binary/1)
|
||||
end
|
||||
|
||||
def put_locales(locales) do
|
||||
if is_locale_list(locales) do
|
||||
if locale_list?(locales) do
|
||||
Process.put({Pleroma.Web.Gettext, :locales}, Enum.uniq(locales))
|
||||
Gettext.put_locale(Enum.at(locales, 0, Gettext.get_locale()))
|
||||
:ok
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
||||
alias Pleroma.Web.Utils.Params
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
||||
|
||||
plug(:skip_auth when action in [:create, :lookup])
|
||||
|
||||
|
|
@ -72,7 +72,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
%{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:follows"]} when action in [:relationships, :familiar_followers]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
|
|
@ -92,7 +95,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
plug(
|
||||
RateLimiter,
|
||||
[name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
|
||||
[name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
|
||||
)
|
||||
|
||||
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
|
||||
|
|
@ -104,7 +107,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
|
||||
|
||||
@doc "POST /api/v1/accounts"
|
||||
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
||||
def create(
|
||||
%{assigns: %{app: app}, private: %{open_api_spex: %{body_params: params}}} = conn,
|
||||
_params
|
||||
) do
|
||||
with :ok <- validate_email_param(params),
|
||||
:ok <- TwitterAPI.validate_captcha(app, params),
|
||||
{:ok, user} <- TwitterAPI.register_user(params),
|
||||
|
|
@ -168,7 +174,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "PATCH /api/v1/accounts/update_credentials"
|
||||
def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
|
||||
def update_credentials(
|
||||
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn,
|
||||
_params
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
|
||||
|
|
@ -235,7 +244,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
# So we first build the normal local changeset, then apply it to the
|
||||
# user data, but don't persist it. With this, we generate the object
|
||||
# data for our update activity. We feed this and the changeset as meta
|
||||
# inforation into the pipeline, where they will be properly updated and
|
||||
# information into the pipeline, where they will be properly updated and
|
||||
# federated.
|
||||
with changeset <- User.update_changeset(user, user_params),
|
||||
{:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
|
||||
|
|
@ -263,6 +272,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
{:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} ->
|
||||
render_error(conn, :request_entity_too_large, "File is too large")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:bio, {_, _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "Bio is too long")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "Name is too long")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "One or more field entries are too long")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:fields, {_, _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "Too many field entries")
|
||||
|
||||
_e ->
|
||||
render_error(conn, :forbidden, "Invalid request")
|
||||
end
|
||||
|
|
@ -277,7 +298,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "GET /api/v1/accounts/relationships"
|
||||
def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||
def relationships(
|
||||
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
|
||||
_
|
||||
) do
|
||||
targets = User.get_all_by_ids(List.wrap(id))
|
||||
|
||||
render(conn, "relationships.json", user: user, targets: targets)
|
||||
|
|
@ -287,7 +311,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
|
||||
|
||||
@doc "GET /api/v1/accounts/:id"
|
||||
def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
|
||||
def show(
|
||||
%{
|
||||
assigns: %{user: for_user},
|
||||
private: %{open_api_spex: %{params: %{id: nickname_or_id} = params}}
|
||||
} = conn,
|
||||
_params
|
||||
) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
|
||||
:visible <- User.visible_for(user, for_user) do
|
||||
render(conn, "show.json",
|
||||
|
|
@ -301,7 +331,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "GET /api/v1/accounts/:id/statuses"
|
||||
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
|
||||
def statuses(
|
||||
%{assigns: %{user: reading_user}, private: %{open_api_spex: %{params: params}}} = conn,
|
||||
_params
|
||||
) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
|
||||
:visible <- User.visible_for(user, reading_user) do
|
||||
params =
|
||||
|
|
@ -336,7 +369,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "GET /api/v1/accounts/:id/followers"
|
||||
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
|
||||
def followers(
|
||||
%{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
|
||||
conn,
|
||||
_params
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|
||||
|
|
@ -361,7 +398,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "GET /api/v1/accounts/:id/following"
|
||||
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
|
||||
def following(
|
||||
%{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
|
||||
conn,
|
||||
_params
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|
||||
|
|
@ -399,7 +440,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
{:error, "Can not follow yourself"}
|
||||
end
|
||||
|
||||
def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
|
||||
def follow(
|
||||
%{
|
||||
assigns: %{user: follower, account: followed},
|
||||
private: %{open_api_spex: %{body_params: params}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
|
||||
render(conn, "relationship.json", user: follower, target: followed)
|
||||
else
|
||||
|
|
@ -419,7 +466,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "POST /api/v1/accounts/:id/mute"
|
||||
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
|
||||
def mute(
|
||||
%{
|
||||
assigns: %{user: muter, account: muted},
|
||||
private: %{open_api_spex: %{body_params: params}}
|
||||
} = conn,
|
||||
_params
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|> Map.put_new(:duration, Map.get(params, :expires_in, 0))
|
||||
|
|
@ -460,7 +513,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
@doc "POST /api/v1/accounts/:id/note"
|
||||
def note(
|
||||
%{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
|
||||
%{
|
||||
assigns: %{user: noter, account: target},
|
||||
private: %{open_api_spex: %{body_params: %{comment: comment}}}
|
||||
} = conn,
|
||||
_params
|
||||
) do
|
||||
with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
|
||||
|
|
@ -501,7 +557,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "POST /api/v1/follows"
|
||||
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
|
||||
def follow_by_uri(%{private: %{open_api_spex: %{body_params: %{uri: uri}}}} = conn, _) do
|
||||
case User.get_cached_by_nickname(uri) do
|
||||
%User{} = user ->
|
||||
conn
|
||||
|
|
@ -540,11 +596,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
conn
|
||||
|> add_link_headers(users)
|
||||
|> render("index.json", users: users, for: user, as: :user)
|
||||
|> render("index.json",
|
||||
users: users,
|
||||
for: user,
|
||||
as: :user,
|
||||
embed_relationships: embed_relationships?(params)
|
||||
)
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/accounts/lookup"
|
||||
def lookup(conn, %{acct: nickname} = _params) do
|
||||
def lookup(%{private: %{open_api_spex: %{params: %{acct: nickname}}}} = conn, _params) do
|
||||
with %User{} = user <- User.get_by_nickname(nickname) do
|
||||
render(conn, "show.json",
|
||||
user: user,
|
||||
|
|
@ -571,6 +632,35 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
)
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/accounts/familiar_followers"
|
||||
def familiar_followers(
|
||||
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
|
||||
_id
|
||||
) do
|
||||
users =
|
||||
User.get_all_by_ids(List.wrap(id))
|
||||
|> Enum.map(&%{id: &1.id, accounts: get_familiar_followers(&1, user)})
|
||||
|
||||
conn
|
||||
|> render("familiar_followers.json",
|
||||
for: user,
|
||||
users: users,
|
||||
as: :user
|
||||
)
|
||||
end
|
||||
|
||||
defp get_familiar_followers(%{id: id} = user, %{id: id}) do
|
||||
User.get_familiar_followers(user, user)
|
||||
end
|
||||
|
||||
defp get_familiar_followers(%{hide_followers: true}, _current_user) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp get_familiar_followers(user, current_user) do
|
||||
User.get_familiar_followers(user, current_user)
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/identity_proofs"
|
||||
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.DirectoryController do
|
|||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(:skip_auth when action == "index")
|
||||
plug(:skip_auth when action == :index)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DirectoryOperation
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
|
||||
|
||||
plug(
|
||||
|
|
@ -27,23 +27,31 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
|
|||
end
|
||||
|
||||
@doc "POST /api/v1/domain_blocks"
|
||||
def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
|
||||
def create(
|
||||
%{assigns: %{user: blocker}, private: %{open_api_spex: %{body_params: %{domain: domain}}}} =
|
||||
conn,
|
||||
_params
|
||||
) do
|
||||
User.block_domain(blocker, domain)
|
||||
json(conn, %{})
|
||||
end
|
||||
|
||||
def create(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
|
||||
def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
|
||||
User.block_domain(blocker, domain)
|
||||
json(conn, %{})
|
||||
end
|
||||
|
||||
@doc "DELETE /api/v1/domain_blocks"
|
||||
def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
|
||||
def delete(
|
||||
%{assigns: %{user: blocker}, private: %{open_api_spex: %{body_params: %{domain: domain}}}} =
|
||||
conn,
|
||||
_params
|
||||
) do
|
||||
User.unblock_domain(blocker, domain)
|
||||
json(conn, %{})
|
||||
end
|
||||
|
||||
def delete(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
|
||||
def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
|
||||
User.unblock_domain(blocker, domain)
|
||||
json(conn, %{})
|
||||
end
|
||||
|
|
|
|||
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