Merge remote-tracking branch 'origin/develop' into language-detection
This commit is contained in:
commit
2b1ef1bbdf
1005 changed files with 13890 additions and 4733 deletions
|
|
@ -201,7 +201,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
def notify_and_stream(activity) do
|
||||
{:ok, notifications} = Notification.create_notifications(activity)
|
||||
Notification.send(notifications)
|
||||
Notification.stream(notifications)
|
||||
|
||||
original_activity =
|
||||
case activity do
|
||||
|
|
@ -222,10 +222,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
%{data: %{"expires_at" => %DateTime{} = expires_at}} = activity
|
||||
) do
|
||||
with {:ok, _job} <-
|
||||
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
|
||||
activity_id: activity.id,
|
||||
expires_at: expires_at
|
||||
}) do
|
||||
Pleroma.Workers.PurgeExpiredActivity.enqueue(
|
||||
%{
|
||||
activity_id: activity.id
|
||||
},
|
||||
scheduled_at: expires_at
|
||||
) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
|
@ -446,10 +448,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
_ <- notify_and_stream(activity) do
|
||||
maybe_federate(activity)
|
||||
|
||||
BackgroundWorker.enqueue("move_following", %{
|
||||
BackgroundWorker.new(%{
|
||||
"op" => "move_following",
|
||||
"origin_id" => origin.id,
|
||||
"target_id" => target.id
|
||||
})
|
||||
|> Oban.insert()
|
||||
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
@ -920,6 +924,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
)
|
||||
end
|
||||
|
||||
# Essentially, either look for activities addressed to `recipients`, _OR_ ones
|
||||
# that reference a hashtag that the user follows
|
||||
# Firstly, two fallbacks in case there's no hashtag constraint, or the user doesn't
|
||||
# follow any
|
||||
defp restrict_recipients_or_hashtags(query, recipients, user, nil) do
|
||||
restrict_recipients(query, recipients, user)
|
||||
end
|
||||
|
||||
defp restrict_recipients_or_hashtags(query, recipients, user, []) do
|
||||
restrict_recipients(query, recipients, user)
|
||||
end
|
||||
|
||||
defp restrict_recipients_or_hashtags(query, recipients, _user, hashtag_ids) do
|
||||
from([activity, object] in query)
|
||||
|> join(:left, [activity, object], hto in "hashtags_objects",
|
||||
on: hto.object_id == object.id,
|
||||
as: :hto
|
||||
)
|
||||
|> where(
|
||||
[activity, object, hto: hto],
|
||||
(hto.hashtag_id in ^hashtag_ids and ^Constants.as_public() in activity.recipients) or
|
||||
fragment("? && ?", ^recipients, activity.recipients)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_local(query, %{local_only: true}) do
|
||||
from(activity in query, where: activity.local == true)
|
||||
end
|
||||
|
|
@ -979,8 +1008,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
|
||||
|
||||
|
|
@ -1409,7 +1439,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> maybe_preload_report_notes(opts)
|
||||
|> maybe_set_thread_muted_field(opts)
|
||||
|> maybe_order(opts)
|
||||
|> restrict_recipients(recipients, opts[:user])
|
||||
|> restrict_recipients_or_hashtags(recipients, opts[:user], opts[:followed_hashtags])
|
||||
|> restrict_replies(opts)
|
||||
|> restrict_since(opts)
|
||||
|> restrict_local(opts)
|
||||
|
|
@ -1537,16 +1567,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp get_actor_url(_url), do: nil
|
||||
|
||||
defp normalize_image(%{"url" => url}) do
|
||||
defp normalize_image(%{"url" => url} = data) do
|
||||
%{
|
||||
"type" => "Image",
|
||||
"url" => [%{"href" => url}]
|
||||
}
|
||||
|> maybe_put_description(data)
|
||||
end
|
||||
|
||||
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
|
||||
defp normalize_image(_), do: nil
|
||||
|
||||
defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do
|
||||
Map.put(map, "name", description)
|
||||
end
|
||||
|
||||
defp maybe_put_description(map, _), do: map
|
||||
|
||||
defp object_to_user_data(data, additional) do
|
||||
fields =
|
||||
data
|
||||
|
|
@ -1660,7 +1697,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
}}
|
||||
else
|
||||
{:error, _} = e -> e
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1793,24 +1829,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
def pinned_fetch_task(nil), do: nil
|
||||
|
||||
def pinned_fetch_task(%{pinned_objects: pins}) do
|
||||
if Enum.all?(pins, fn {ap_id, _} ->
|
||||
Object.get_cached_by_ap_id(ap_id) ||
|
||||
match?({:ok, _object}, Fetcher.fetch_object_from_id(ap_id))
|
||||
end) do
|
||||
:ok
|
||||
else
|
||||
:error
|
||||
end
|
||||
def enqueue_pin_fetches(%{pinned_objects: pins}) do
|
||||
# enqueue a task to fetch all pinned objects
|
||||
Enum.each(pins, fn {ap_id, _} ->
|
||||
if is_nil(Object.get_cached_by_ap_id(ap_id)) do
|
||||
Pleroma.Workers.RemoteFetcherWorker.new(%{
|
||||
"op" => "fetch_remote",
|
||||
"id" => ap_id,
|
||||
"depth" => 1
|
||||
})
|
||||
|> Oban.insert()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def enqueue_pin_fetches(_), do: nil
|
||||
|
||||
def make_user_from_ap_id(ap_id, additional \\ []) do
|
||||
user = User.get_cached_by_ap_id(ap_id)
|
||||
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
|
||||
enqueue_pin_fetches(data)
|
||||
|
||||
if user do
|
||||
user
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
when action in [:activity, :object]
|
||||
)
|
||||
|
||||
plug(:log_inbox_metadata when action in [:inbox])
|
||||
plug(:set_requester_reachable when action in [:inbox])
|
||||
plug(:relay_active? when action in [:relay])
|
||||
|
||||
|
|
@ -292,8 +293,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
json(conn, "ok")
|
||||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: false}, req_headers: req_headers} = conn, params) do
|
||||
Federator.incoming_ap_doc(%{req_headers: req_headers, params: params})
|
||||
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
|
||||
Federator.incoming_ap_doc(%{
|
||||
method: conn.method,
|
||||
req_headers: conn.req_headers,
|
||||
request_path: conn.request_path,
|
||||
params: params,
|
||||
query_string: conn.query_string
|
||||
})
|
||||
|
||||
json(conn, "ok")
|
||||
end
|
||||
|
||||
|
|
@ -303,7 +311,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
post_inbox_relayed_create(conn, params)
|
||||
else
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> put_status(403)
|
||||
|> json("Not federating")
|
||||
end
|
||||
end
|
||||
|
|
@ -474,7 +482,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|> put_status(:forbidden)
|
||||
|> json(message)
|
||||
|
||||
{:error, message} ->
|
||||
{:error, message} when is_binary(message) ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(message)
|
||||
|
|
@ -521,6 +529,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
conn
|
||||
end
|
||||
|
||||
defp log_inbox_metadata(%{params: %{"actor" => actor, "type" => type}} = conn, _) do
|
||||
Logger.metadata(actor: actor, type: type)
|
||||
conn
|
||||
end
|
||||
|
||||
defp log_inbox_metadata(conn, _), do: conn
|
||||
|
||||
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
|
||||
with {:ok, object} <-
|
||||
ActivityPub.upload(
|
||||
|
|
|
|||
|
|
@ -108,6 +108,14 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
|
||||
def filter(%{} = object), do: get_policies() |> filter(object)
|
||||
|
||||
def id_filter(policies, id) when is_binary(id) do
|
||||
policies
|
||||
|> Enum.filter(&function_exported?(&1, :id_filter, 1))
|
||||
|> Enum.all?(& &1.id_filter(id))
|
||||
end
|
||||
|
||||
def id_filter(id) when is_binary(id), do: get_policies() |> id_filter(id)
|
||||
|
||||
@impl true
|
||||
def pipeline_filter(%{} = message, meta) do
|
||||
object = meta[:object_data]
|
||||
|
|
@ -204,7 +212,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
if function_exported?(policy, :config_description, 0) do
|
||||
description =
|
||||
@default_description
|
||||
|> Map.merge(policy.config_description)
|
||||
|> Map.merge(policy.config_description())
|
||||
|> Map.put(:group, :pleroma)
|
||||
|> Map.put(:tab, :mrf)
|
||||
|> Map.put(:type, :group)
|
||||
|
|
|
|||
|
|
@ -63,20 +63,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
|
||||
def filter(%{"type" => "Follow", "actor" => actor_id} = activity) do
|
||||
%User{} = actor = normalize_by_ap_id(actor_id)
|
||||
|
||||
score = determine_if_followbot(actor)
|
||||
|
||||
if score < 0.8 || bot_allowed?(message, actor) do
|
||||
{:ok, message}
|
||||
if score < 0.8 || bot_allowed?(activity, actor) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
|
|||
|
|
@ -29,17 +29,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
|
|||
defp contains_links?(_), do: false
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
|
||||
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = activity) do
|
||||
with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor),
|
||||
{:contains_links, true} <- {:contains_links, contains_links?(object)},
|
||||
{:old_user, true} <- {:old_user, old_user?(u)} do
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
{:ok, %User{local: true}} ->
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
|
||||
{:contains_links, false} ->
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
|
||||
{:old_user, false} ->
|
||||
{:reject, "[AntiLinkSpamPolicy] User has no posts nor followers"}
|
||||
|
|
@ -53,7 +53,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
|
|||
end
|
||||
|
||||
# in all other cases, pass through
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
|
|||
87
lib/pleroma/web/activity_pub/mrf/anti_mention_spam_policy.ex
Normal file
87
lib/pleroma/web/activity_pub/mrf/anti_mention_spam_policy.ex
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.AntiMentionSpamPolicy do
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
require Pleroma.Constants
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp user_has_posted?(%User{} = u), do: u.note_count > 0
|
||||
|
||||
defp user_has_age?(%User{} = u) do
|
||||
user_age_limit = Config.get([:mrf_antimentionspam, :user_age_limit], 30_000)
|
||||
diff = NaiveDateTime.utc_now() |> NaiveDateTime.diff(u.inserted_at, :millisecond)
|
||||
diff >= user_age_limit
|
||||
end
|
||||
|
||||
defp good_reputation?(%User{} = u) do
|
||||
user_has_age?(u) and user_has_posted?(u)
|
||||
end
|
||||
|
||||
# copied from HellthreadPolicy
|
||||
defp get_recipient_count(activity) do
|
||||
recipients = (activity["to"] || []) ++ (activity["cc"] || [])
|
||||
|
||||
follower_collection =
|
||||
User.get_cached_by_ap_id(activity["actor"] || activity["attributedTo"]).follower_address
|
||||
|
||||
if Enum.member?(recipients, Pleroma.Constants.as_public()) do
|
||||
recipients =
|
||||
recipients
|
||||
|> List.delete(Pleroma.Constants.as_public())
|
||||
|> List.delete(follower_collection)
|
||||
|
||||
{:public, length(recipients)}
|
||||
else
|
||||
recipients =
|
||||
recipients
|
||||
|> List.delete(follower_collection)
|
||||
|
||||
{:not_public, length(recipients)}
|
||||
end
|
||||
end
|
||||
|
||||
defp object_has_recipients?(%{"object" => object} = activity) do
|
||||
{_, object_count} = get_recipient_count(object)
|
||||
{_, activity_count} = get_recipient_count(activity)
|
||||
object_count + activity_count > 0
|
||||
end
|
||||
|
||||
defp object_has_recipients?(object) do
|
||||
{_, count} = get_recipient_count(object)
|
||||
count > 0
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "actor" => actor} = activity) do
|
||||
with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor),
|
||||
{:has_mentions, true} <- {:has_mentions, object_has_recipients?(activity)},
|
||||
{:good_reputation, true} <- {:good_reputation, good_reputation?(u)} do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:ok, %User{local: true}} ->
|
||||
{:ok, activity}
|
||||
|
||||
{:has_mentions, false} ->
|
||||
{:ok, activity}
|
||||
|
||||
{:good_reputation, false} ->
|
||||
{:reject, "[AntiMentionSpamPolicy] User rejected"}
|
||||
|
||||
{:error, _} ->
|
||||
{:reject, "[AntiMentionSpamPolicy] Failed to get or fetch user by ap_id"}
|
||||
|
||||
e ->
|
||||
{:reject, "[AntiMentionSpamPolicy] Unhandled error #{inspect(e)}"}
|
||||
end
|
||||
end
|
||||
|
||||
# in all other cases, pass through
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
end
|
||||
146
lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex
Normal file
146
lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# 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.DNSRBLPolicy do
|
||||
@moduledoc """
|
||||
Dynamic activity filtering based on an RBL database
|
||||
|
||||
This MRF makes queries to a custom DNS server which will
|
||||
respond with values indicating the classification of the domain
|
||||
the activity originated from. This method has been widely used
|
||||
in the email anti-spam industry for very fast reputation checks.
|
||||
|
||||
e.g., if the DNS response is 127.0.0.1 or empty, the domain is OK
|
||||
Other values such as 127.0.0.2 may be used for specific classifications.
|
||||
|
||||
Information for why the host is blocked can be stored in a corresponding TXT record.
|
||||
|
||||
This method is fail-open so if the queries fail the activites are accepted.
|
||||
|
||||
An example of software meant for this purpsoe is rbldnsd which can be found
|
||||
at http://www.corpit.ru/mjt/rbldnsd.html or mirrored at
|
||||
https://git.pleroma.social/feld/rbldnsd
|
||||
|
||||
It is highly recommended that you run your own copy of rbldnsd and use an
|
||||
external mechanism to sync/share the contents of the zone file. This is
|
||||
important to keep the latency on the queries as low as possible and prevent
|
||||
your DNS server from being attacked so it fails and content is permitted.
|
||||
"""
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
|
||||
require Logger
|
||||
|
||||
@query_retries 1
|
||||
@query_timeout 500
|
||||
|
||||
@impl true
|
||||
def filter(%{"actor" => actor} = activity) do
|
||||
actor_info = URI.parse(actor)
|
||||
|
||||
with {:ok, activity} <- check_rbl(actor_info, activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_ -> {:reject, "[DNSRBLPolicy]"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe do
|
||||
mrf_dnsrbl =
|
||||
Config.get(:mrf_dnsrbl)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{:ok, %{mrf_dnsrbl: mrf_dnsrbl}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def config_description do
|
||||
%{
|
||||
key: :mrf_dnsrbl,
|
||||
related_policy: "Pleroma.Web.ActivityPub.MRF.DNSRBLPolicy",
|
||||
label: "MRF DNSRBL",
|
||||
description: "DNS RealTime Blackhole Policy",
|
||||
children: [
|
||||
%{
|
||||
key: :nameserver,
|
||||
type: {:string},
|
||||
description: "DNSRBL Nameserver to Query (IP or hostame)",
|
||||
suggestions: ["127.0.0.1"]
|
||||
},
|
||||
%{
|
||||
key: :port,
|
||||
type: {:string},
|
||||
description: "Nameserver port",
|
||||
suggestions: ["53"]
|
||||
},
|
||||
%{
|
||||
key: :zone,
|
||||
type: {:string},
|
||||
description: "Root zone for querying",
|
||||
suggestions: ["bl.pleroma.com"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp check_rbl(%{host: actor_host}, activity) do
|
||||
with false <- match?(^actor_host, Pleroma.Web.Endpoint.host()),
|
||||
zone when not is_nil(zone) <- Keyword.get(Config.get([:mrf_dnsrbl]), :zone) do
|
||||
query =
|
||||
Enum.join([actor_host, zone], ".")
|
||||
|> String.to_charlist()
|
||||
|
||||
rbl_response = rblquery(query)
|
||||
|
||||
if Enum.empty?(rbl_response) do
|
||||
{:ok, activity}
|
||||
else
|
||||
Task.start(fn ->
|
||||
reason =
|
||||
case rblquery(query, :txt) do
|
||||
[[result]] -> result
|
||||
_ -> "undefined"
|
||||
end
|
||||
|
||||
Logger.warning(
|
||||
"DNSRBL Rejected activity from #{actor_host} for reason: #{inspect(reason)}"
|
||||
)
|
||||
end)
|
||||
|
||||
:error
|
||||
end
|
||||
else
|
||||
_ -> {:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_rblhost_ip(rblhost) do
|
||||
case rblhost |> String.to_charlist() |> :inet_parse.address() do
|
||||
{:ok, _} -> rblhost |> String.to_charlist() |> :inet_parse.address()
|
||||
_ -> {:ok, rblhost |> String.to_charlist() |> :inet_res.lookup(:in, :a) |> Enum.random()}
|
||||
end
|
||||
end
|
||||
|
||||
defp rblquery(query, type \\ :a) do
|
||||
config = Config.get([:mrf_dnsrbl])
|
||||
|
||||
case get_rblhost_ip(config[:nameserver]) do
|
||||
{:ok, rblnsip} ->
|
||||
:inet_res.lookup(query, :in, type,
|
||||
nameservers: [{rblnsip, config[:port]}],
|
||||
timeout: @query_timeout,
|
||||
retry: @query_retries
|
||||
)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -8,9 +8,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
|
|||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(object) do
|
||||
Logger.debug("REJECTING #{inspect(object)}")
|
||||
{:reject, object}
|
||||
def filter(activity) do
|
||||
Logger.debug("REJECTING #{inspect(activity)}")
|
||||
{:reject, activity}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def id_filter(id) do
|
||||
Logger.debug("REJECTING #{id}")
|
||||
false
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_shortcode], [])
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@impl true
|
||||
def history_awareness, do: :manual
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(%{"type" => type, "object" => %{"type" => objtype} = object} = message)
|
||||
@impl true
|
||||
def filter(%{"type" => type, "object" => %{"type" => objtype} = object} = activity)
|
||||
when type in ["Create", "Update"] and objtype in Pleroma.Constants.status_object_types() do
|
||||
with {:ok, object} <-
|
||||
Updater.do_with_history(object, fn object ->
|
||||
|
|
@ -42,13 +42,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
Updater.do_with_history(object, fn object ->
|
||||
{:ok, process_remove(object, :shortcode, config_remove_shortcode())}
|
||||
end),
|
||||
activity <- Map.put(message, "object", object),
|
||||
activity <- Map.put(activity, "object", object),
|
||||
activity <- maybe_delist(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@impl true
|
||||
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
|
||||
|
|
@ -56,7 +56,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
end
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@impl true
|
||||
def filter(%{"type" => "EmojiReact"} = object) do
|
||||
with {:ok, _} <-
|
||||
matched_emoji_checker(config_remove_url(), config_remove_shortcode()).(object) do
|
||||
|
|
@ -67,9 +67,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
end
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(message) do
|
||||
{:ok, message}
|
||||
@impl true
|
||||
def filter(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
defp match_string?(string, pattern) when is_binary(pattern) do
|
||||
|
|
@ -214,7 +214,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
)
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@impl true
|
||||
def describe do
|
||||
mrf_emoji =
|
||||
Pleroma.Config.get(:mrf_emoji, [])
|
||||
|
|
@ -226,7 +226,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
{:ok, %{mrf_emoji: mrf_emoji}}
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@impl true
|
||||
def config_description do
|
||||
%{
|
||||
key: :mrf_emoji,
|
||||
|
|
@ -239,7 +239,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
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.
|
||||
A list of patterns which result in emoji whose URL matches being removed from the activity. 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/`.
|
||||
""",
|
||||
|
|
@ -249,7 +249,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
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.
|
||||
A list of patterns which result in emoji whose shortcode matches being removed from the activity. 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/`.
|
||||
""",
|
||||
|
|
@ -259,7 +259,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
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.
|
||||
A list of patterns which result in activity 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/`.
|
||||
""",
|
||||
|
|
@ -269,7 +269,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
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.
|
||||
A list of patterns which result in activities 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/`.
|
||||
""",
|
||||
|
|
|
|||
|
|
@ -29,19 +29,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
|
|||
|
||||
def filter_by_summary(_in_reply_to, child), do: child
|
||||
|
||||
def filter(%{"type" => type, "object" => child_object} = object)
|
||||
when type in ["Create", "Update"] and is_map(child_object) do
|
||||
def filter(%{"type" => type, "object" => object} = activity)
|
||||
when type in ["Create", "Update"] and is_map(object) do
|
||||
child =
|
||||
child_object["inReplyTo"]
|
||||
object["inReplyTo"]
|
||||
|> Object.normalize(fetch: false)
|
||||
|> filter_by_summary(child_object)
|
||||
|> filter_by_summary(object)
|
||||
|
||||
object = Map.put(object, "object", child)
|
||||
activity = Map.put(activity, "object", child)
|
||||
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
def filter(object), do: {:ok, object}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
def describe, do: {:ok, %{}}
|
||||
end
|
||||
|
|
|
|||
53
lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex
Normal file
53
lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# 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.FODirectReply do
|
||||
@moduledoc """
|
||||
FODirectReply alters the scope of replies to activities which are Followers Only to be Direct. The purpose of this policy is to prevent broken threads for followers of the reply author because their response was to a user that they are not also following.
|
||||
"""
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(
|
||||
%{
|
||||
"type" => "Create",
|
||||
"to" => to,
|
||||
"object" => %{
|
||||
"actor" => actor,
|
||||
"type" => "Note",
|
||||
"inReplyTo" => in_reply_to
|
||||
}
|
||||
} = activity
|
||||
) do
|
||||
with true <- is_binary(in_reply_to),
|
||||
%User{follower_address: followers_collection, local: true} <- User.get_by_ap_id(actor),
|
||||
%Object{} = in_reply_to_object <- Object.get_by_ap_id(in_reply_to),
|
||||
"private" <- Visibility.get_visibility(in_reply_to_object) do
|
||||
direct_to = to -- [followers_collection]
|
||||
|
||||
updated_activity =
|
||||
activity
|
||||
|> Map.put("cc", [])
|
||||
|> Map.put("to", direct_to)
|
||||
|> Map.put("directMessage", true)
|
||||
|> put_in(["object", "cc"], [])
|
||||
|> put_in(["object", "to"], direct_to)
|
||||
|
||||
{:ok, updated_activity}
|
||||
else
|
||||
_ -> {:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
end
|
||||
|
|
@ -11,12 +11,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
|
|||
require Logger
|
||||
|
||||
@impl true
|
||||
def filter(message) do
|
||||
def filter(activity) do
|
||||
with follower_nickname <- Config.get([:mrf_follow_bot, :follower_nickname]),
|
||||
%User{actor_type: "Service"} = follower <-
|
||||
User.get_cached_by_nickname(follower_nickname),
|
||||
%{"type" => "Create", "object" => %{"type" => "Note"}} <- message do
|
||||
try_follow(follower, message)
|
||||
%{"type" => "Create", "object" => %{"type" => "Note"}} <- activity do
|
||||
try_follow(follower, activity)
|
||||
else
|
||||
nil ->
|
||||
Logger.warning(
|
||||
|
|
@ -24,17 +24,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
|
|||
account does not exist, or the account is not correctly configured as a bot."
|
||||
)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
|
||||
_ ->
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp try_follow(follower, message) do
|
||||
to = Map.get(message, "to", [])
|
||||
cc = Map.get(message, "cc", [])
|
||||
actor = [message["actor"]]
|
||||
defp try_follow(follower, activity) do
|
||||
to = Map.get(activity, "to", [])
|
||||
cc = Map.get(activity, "cc", [])
|
||||
actor = [activity["actor"]]
|
||||
|
||||
Enum.concat([to, cc, actor])
|
||||
|> List.flatten()
|
||||
|
|
@ -49,11 +49,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
|
|||
"#{__MODULE__}: Follow request from #{follower.nickname} to #{user.nickname}"
|
||||
)
|
||||
|
||||
CommonAPI.follow(follower, user)
|
||||
CommonAPI.follow(user, follower)
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do
|
|||
"cc" => cc,
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
} = message
|
||||
} = activity
|
||||
) do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
isbot = check_if_bot(user)
|
||||
|
|
@ -36,20 +36,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do
|
|||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|
||||
message =
|
||||
message
|
||||
activity =
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|> Map.put("object", object)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(object), do: {:ok, object}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
|
|||
|
|
@ -79,18 +79,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
|
|||
%{
|
||||
"type" => type,
|
||||
"object" => %{"type" => "Note", "to" => to, "inReplyTo" => in_reply_to}
|
||||
} = object
|
||||
} = activity
|
||||
)
|
||||
when type in ["Create", "Update"] and is_list(to) and is_binary(in_reply_to) do
|
||||
# image-only posts from pleroma apparently reach this MRF without the content field
|
||||
content = object["object"]["content"] || ""
|
||||
content = activity["object"]["content"] || ""
|
||||
|
||||
# Get the replied-to user for sorting
|
||||
replied_to_user = get_replied_to_user(object["object"])
|
||||
replied_to_user = get_replied_to_user(activity["object"])
|
||||
|
||||
mention_users =
|
||||
to
|
||||
|> clean_recipients(object)
|
||||
|> clean_recipients(activity)
|
||||
|> Enum.map(&User.get_cached_by_ap_id/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> sort_replied_user(replied_to_user)
|
||||
|
|
@ -126,11 +126,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
|
|||
content
|
||||
end
|
||||
|
||||
{:ok, put_in(object["object"]["content"], content)}
|
||||
{:ok, put_in(activity["object"]["content"], content)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(object), do: {:ok, object}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
|
|||
alias Pleroma.Object
|
||||
|
||||
@moduledoc """
|
||||
Reject, TWKN-remove or Set-Sensitive messages with specific hashtags (without the leading #)
|
||||
Reject, TWKN-remove or Set-Sensitive activities 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.
|
||||
"""
|
||||
|
|
@ -19,40 +19,40 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
|
|||
@impl true
|
||||
def history_awareness, do: :manual
|
||||
|
||||
defp check_reject(message, hashtags) do
|
||||
defp check_reject(activity, hashtags) do
|
||||
if Enum.any?(Config.get([:mrf_hashtag, :reject]), fn match -> match in hashtags end) do
|
||||
{:reject, "[HashtagPolicy] Matches with rejected keyword"}
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_ftl_removal(%{"to" => to} = message, hashtags) do
|
||||
defp check_ftl_removal(%{"to" => to} = activity, hashtags) do
|
||||
if Pleroma.Constants.as_public() in to and
|
||||
Enum.any?(Config.get([:mrf_hashtag, :federated_timeline_removal]), fn match ->
|
||||
match in hashtags
|
||||
end) do
|
||||
to = List.delete(to, Pleroma.Constants.as_public())
|
||||
cc = [Pleroma.Constants.as_public() | message["cc"] || []]
|
||||
cc = [Pleroma.Constants.as_public() | activity["cc"] || []]
|
||||
|
||||
message =
|
||||
message
|
||||
activity =
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|> Kernel.put_in(["object", "to"], to)
|
||||
|> Kernel.put_in(["object", "cc"], cc)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_ftl_removal(message, _hashtags), do: {:ok, message}
|
||||
defp check_ftl_removal(activity, _hashtags), do: {:ok, activity}
|
||||
|
||||
defp check_sensitive(message) do
|
||||
defp check_sensitive(activity) do
|
||||
{:ok, new_object} =
|
||||
Object.Updater.do_with_history(message["object"], fn object ->
|
||||
Object.Updater.do_with_history(activity["object"], fn object ->
|
||||
hashtags = Object.hashtags(%Object{data: object})
|
||||
|
||||
if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do
|
||||
|
|
@ -62,11 +62,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
|
|||
end
|
||||
end)
|
||||
|
||||
{:ok, Map.put(message, "object", new_object)}
|
||||
{:ok, Map.put(activity, "object", new_object)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => type, "object" => object} = message) when type in ["Create", "Update"] do
|
||||
def filter(%{"type" => type, "object" => object} = activity)
|
||||
when type in ["Create", "Update"] do
|
||||
history_items =
|
||||
with %{"formerRepresentations" => %{"orderedItems" => items}} <- object do
|
||||
items
|
||||
|
|
@ -82,23 +83,23 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
|
|||
hashtags = Object.hashtags(%Object{data: object}) ++ historical_hashtags
|
||||
|
||||
if hashtags != [] do
|
||||
with {:ok, message} <- check_reject(message, hashtags),
|
||||
{:ok, message} <-
|
||||
with {:ok, activity} <- check_reject(activity, hashtags),
|
||||
{:ok, activity} <-
|
||||
(if type == "Create" do
|
||||
check_ftl_removal(message, hashtags)
|
||||
check_ftl_removal(activity, hashtags)
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end),
|
||||
{:ok, message} <- check_sensitive(message) do
|
||||
{:ok, message}
|
||||
{:ok, activity} <- check_sensitive(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe do
|
||||
|
|
@ -120,21 +121,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
|
|||
%{
|
||||
key: :reject,
|
||||
type: {:list, :string},
|
||||
description: "A list of hashtags which result in message being rejected.",
|
||||
description: "A list of hashtags which result in the activity being rejected.",
|
||||
suggestions: ["foo"]
|
||||
},
|
||||
%{
|
||||
key: :federated_timeline_removal,
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"A list of hashtags which result in message being removed from federated timelines (a.k.a unlisted).",
|
||||
"A list of hashtags which result in the activity being removed from federated timelines (a.k.a unlisted).",
|
||||
suggestions: ["foo"]
|
||||
},
|
||||
%{
|
||||
key: :sensitive,
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"A list of hashtags which result in message being set as sensitive (a.k.a NSFW/R-18)",
|
||||
"A list of hashtags which result in the activity being set as sensitive (a.k.a NSFW/R-18)",
|
||||
suggestions: ["nsfw", "r18"]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -7,54 +7,54 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
|
|||
|
||||
require Pleroma.Constants
|
||||
|
||||
@moduledoc "Block messages with too much mentions (configurable)"
|
||||
@moduledoc "Block activities with too much mentions (configurable)"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp delist_message(message, threshold) when threshold > 0 do
|
||||
follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address
|
||||
to = message["to"] || []
|
||||
cc = message["cc"] || []
|
||||
defp delist_activity(activity, threshold) when threshold > 0 do
|
||||
follower_collection = User.get_cached_by_ap_id(activity["actor"]).follower_address
|
||||
to = activity["to"] || []
|
||||
cc = activity["cc"] || []
|
||||
|
||||
follower_collection? = Enum.member?(to ++ cc, follower_collection)
|
||||
|
||||
message =
|
||||
case get_recipient_count(message) do
|
||||
activity =
|
||||
case get_recipient_count(activity) do
|
||||
{:public, recipients}
|
||||
when follower_collection? and recipients > threshold ->
|
||||
message
|
||||
activity
|
||||
|> Map.put("to", [follower_collection])
|
||||
|> Map.put("cc", [Pleroma.Constants.as_public()])
|
||||
|
||||
{:public, recipients} when recipients > threshold ->
|
||||
message
|
||||
activity
|
||||
|> Map.put("to", [])
|
||||
|> Map.put("cc", [Pleroma.Constants.as_public()])
|
||||
|
||||
_ ->
|
||||
message
|
||||
activity
|
||||
end
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
defp delist_message(message, _threshold), do: {:ok, message}
|
||||
defp delist_activity(activity, _threshold), do: {:ok, activity}
|
||||
|
||||
defp reject_message(message, threshold) when threshold > 0 do
|
||||
with {_, recipients} <- get_recipient_count(message) do
|
||||
defp reject_activity(activity, threshold) when threshold > 0 do
|
||||
with {_, recipients} <- get_recipient_count(activity) do
|
||||
if recipients > threshold do
|
||||
{:reject, "[HellthreadPolicy] #{recipients} recipients is over the limit of #{threshold}"}
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp reject_message(message, _threshold), do: {:ok, message}
|
||||
defp reject_activity(activity, _threshold), do: {:ok, activity}
|
||||
|
||||
defp get_recipient_count(message) do
|
||||
recipients = (message["to"] || []) ++ (message["cc"] || [])
|
||||
follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address
|
||||
defp get_recipient_count(activity) do
|
||||
recipients = (activity["to"] || []) ++ (activity["cc"] || [])
|
||||
follower_collection = User.get_cached_by_ap_id(activity["actor"]).follower_address
|
||||
|
||||
if Enum.member?(recipients, Pleroma.Constants.as_public()) do
|
||||
recipients =
|
||||
|
|
@ -73,7 +73,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message)
|
||||
def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = activity)
|
||||
when object_type in ~w{Note Article} do
|
||||
reject_threshold =
|
||||
Pleroma.Config.get(
|
||||
|
|
@ -83,16 +83,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
|
|||
|
||||
delist_threshold = Pleroma.Config.get([:mrf_hellthread, :delist_threshold])
|
||||
|
||||
with {:ok, message} <- reject_message(message, reject_threshold),
|
||||
{:ok, message} <- delist_message(message, delist_threshold) do
|
||||
{:ok, message}
|
||||
with {:ok, activity} <- reject_activity(activity, reject_threshold),
|
||||
{:ok, activity} <- delist_activity(activity, delist_threshold) do
|
||||
{:ok, activity}
|
||||
else
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe,
|
||||
|
|
@ -104,13 +104,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
|
|||
key: :mrf_hellthread,
|
||||
related_policy: "Pleroma.Web.ActivityPub.MRF.HellthreadPolicy",
|
||||
label: "MRF Hellthread",
|
||||
description: "Block messages with excessive user mentions",
|
||||
description: "Block activities with excessive user mentions",
|
||||
children: [
|
||||
%{
|
||||
key: :delist_threshold,
|
||||
type: :integer,
|
||||
description:
|
||||
"Number of mentioned users after which the message gets removed from timelines and" <>
|
||||
"Number of mentioned users after which the activity gets removed from timelines and" <>
|
||||
"disables notifications. Set to 0 to disable.",
|
||||
suggestions: [10]
|
||||
},
|
||||
|
|
@ -118,7 +118,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
|
|||
key: :reject_threshold,
|
||||
type: :integer,
|
||||
description:
|
||||
"Number of mentioned users after which the messaged gets rejected. Set to 0 to disable.",
|
||||
"Number of mentioned users after which the activity gets rejected. Set to 0 to disable.",
|
||||
suggestions: [20]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(object), do: {:ok, object}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@impl true
|
||||
def history_awareness, do: :auto
|
||||
|
||||
@impl true
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
|
||||
alias Pleroma.Web.ActivityPub.MRF.Utils
|
||||
|
||||
@moduledoc "Reject or Word-Replace messages with a keyword or regex"
|
||||
@moduledoc "Reject or Word-Replace activities with a keyword or regex"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
|> Enum.join("\n")
|
||||
end
|
||||
|
||||
defp check_reject(%{"object" => %{} = object} = message) do
|
||||
defp check_reject(%{"object" => %{} = object} = activity) do
|
||||
with {:ok, _new_object} <-
|
||||
Pleroma.Object.Updater.do_with_history(object, fn object ->
|
||||
payload = object_payload(object)
|
||||
|
|
@ -35,16 +35,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
end) do
|
||||
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end) do
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
defp check_ftl_removal(%{"type" => "Create", "to" => to, "object" => %{} = object} = message) do
|
||||
defp check_ftl_removal(%{"type" => "Create", "to" => to, "object" => %{} = object} = activity) do
|
||||
check_keyword = fn object ->
|
||||
payload = object_payload(object)
|
||||
|
||||
|
|
@ -67,24 +67,24 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
|
||||
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() | message["cc"] || []]
|
||||
cc = [Pleroma.Constants.as_public() | activity["cc"] || []]
|
||||
|
||||
message =
|
||||
message
|
||||
activity =
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_ftl_removal(message) do
|
||||
{:ok, message}
|
||||
defp check_ftl_removal(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
defp check_replace(%{"object" => %{} = object} = message) do
|
||||
defp check_replace(%{"object" => %{} = object} = activity) do
|
||||
replace_kw = fn object ->
|
||||
["content", "name", "summary"]
|
||||
|> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)
|
||||
|
|
@ -103,18 +103,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
|
||||
{:ok, object} = Pleroma.Object.Updater.do_with_history(object, replace_kw)
|
||||
|
||||
message = Map.put(message, "object", object)
|
||||
activity = Map.put(activity, "object", object)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => type, "object" => %{"content" => _content}} = message)
|
||||
def filter(%{"type" => type, "object" => %{"content" => _content}} = activity)
|
||||
when type in ["Create", "Update"] do
|
||||
with {:ok, message} <- check_reject(message),
|
||||
{:ok, message} <- check_ftl_removal(message),
|
||||
{:ok, message} <- check_replace(message) do
|
||||
{:ok, message}
|
||||
with {:ok, activity} <- check_reject(activity),
|
||||
{:ok, activity} <- check_ftl_removal(activity),
|
||||
{:ok, activity} <- check_replace(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:reject, nil} -> {:reject, "[KeywordPolicy] "}
|
||||
{:reject, _} = e -> e
|
||||
|
|
@ -123,7 +123,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe do
|
||||
|
|
@ -154,13 +154,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
|
||||
label: "MRF Keyword",
|
||||
description:
|
||||
"Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
|
||||
"Reject or Word-Replace activities matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
|
||||
children: [
|
||||
%{
|
||||
key: :reject,
|
||||
type: {:list, :string},
|
||||
description: """
|
||||
A list of patterns which result in message being rejected.
|
||||
A list of patterns which result in the activity being rejected.
|
||||
|
||||
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||
""",
|
||||
|
|
@ -170,7 +170,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
key: :federated_timeline_removal,
|
||||
type: {:list, :string},
|
||||
description: """
|
||||
A list of patterns which result in message being removed from federated timelines (a.k.a unlisted).
|
||||
A list of patterns which result in the activity being removed from federated timelines (a.k.a unlisted).
|
||||
|
||||
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||
""",
|
||||
|
|
|
|||
|
|
@ -11,11 +11,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
|
|||
|
||||
require Logger
|
||||
|
||||
@adapter_options [
|
||||
pool: :media,
|
||||
recv_timeout: 10_000
|
||||
]
|
||||
|
||||
@impl true
|
||||
def history_awareness, do: :auto
|
||||
|
||||
|
|
@ -27,19 +22,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
|
|||
|
||||
Logger.debug("Prefetching #{inspect(url)} as #{inspect(prefetch_url)}")
|
||||
|
||||
if Pleroma.Config.get(:env) == :test do
|
||||
fetch(prefetch_url)
|
||||
else
|
||||
ConcurrentLimiter.limit(__MODULE__, fn ->
|
||||
Task.start(fn -> fetch(prefetch_url) end)
|
||||
end)
|
||||
end
|
||||
fetch(prefetch_url)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch(url), do: HTTP.get(url, [], @adapter_options)
|
||||
defp fetch(url) do
|
||||
http_client_opts = Pleroma.Config.get([:media_proxy, :proxy_opts, :http], pool: :media)
|
||||
HTTP.get(url, [], http_client_opts)
|
||||
end
|
||||
|
||||
defp preload(%{"object" => %{"attachment" => attachments}} = _message) do
|
||||
defp preload(%{"object" => %{"attachment" => attachments}} = _activity) do
|
||||
Enum.each(attachments, fn
|
||||
%{"url" => url} when is_list(url) ->
|
||||
url
|
||||
|
|
@ -57,15 +49,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => type, "object" => %{"attachment" => attachments} = _object} = message)
|
||||
def filter(%{"type" => type, "object" => %{"attachment" => attachments} = _object} = activity)
|
||||
when type in ["Create", "Update"] and is_list(attachments) and length(attachments) > 0 do
|
||||
preload(message)
|
||||
preload(activity)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
|
|||
|
|
@ -3,25 +3,25 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
|
||||
@moduledoc "Block messages which mention a user"
|
||||
@moduledoc "Block activities which mention a user"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create"} = message) do
|
||||
def filter(%{"type" => "Create"} = activity) do
|
||||
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
|
||||
recipients = (message["to"] || []) ++ (message["cc"] || [])
|
||||
recipients = (activity["to"] || []) ++ (activity["cc"] || [])
|
||||
|
||||
if rejected_mention =
|
||||
Enum.find(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
|
||||
{:reject, "[MentionPolicy] Rejected for mention of #{rejected_mention}"}
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
@ -32,7 +32,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
|
|||
key: :mrf_mention,
|
||||
related_policy: "Pleroma.Web.ActivityPub.MRF.MentionPolicy",
|
||||
label: "MRF Mention",
|
||||
description: "Block messages which mention a specific user",
|
||||
description: "Block activities which mention a specific user",
|
||||
children: [
|
||||
%{
|
||||
key: :actors,
|
||||
|
|
|
|||
|
|
@ -9,20 +9,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
|
|||
alias Pleroma.Web.Endpoint
|
||||
|
||||
@impl true
|
||||
def filter(%{"actor" => actor} = object) do
|
||||
def filter(%{"actor" => actor} = activity) do
|
||||
with true <- local?(actor),
|
||||
true <- eligible_type?(object),
|
||||
true <- note?(object),
|
||||
false <- has_attachment?(object),
|
||||
true <- only_mentions?(object) do
|
||||
true <- eligible_type?(activity),
|
||||
true <- note?(activity),
|
||||
false <- has_attachment?(activity),
|
||||
true <- only_mentions?(activity) do
|
||||
{:reject, "[NoEmptyPolicy]"}
|
||||
else
|
||||
_ ->
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
def filter(object), do: {:ok, object}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
defp local?(actor) do
|
||||
if actor |> String.starts_with?("#{Endpoint.url()}") do
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do
|
|||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(object) do
|
||||
{:ok, object}
|
||||
def filter(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
|||
|
|
@ -13,15 +13,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
|
|||
def filter(
|
||||
%{
|
||||
"type" => type,
|
||||
"object" => %{"content" => content, "attachment" => _} = _child_object
|
||||
} = object
|
||||
"object" => %{"content" => content, "attachment" => _} = _object
|
||||
} = activity
|
||||
)
|
||||
when type in ["Create", "Update"] and content in [".", "<p>.</p>"] do
|
||||
{:ok, put_in(object, ["object", "content"], "")}
|
||||
{:ok, put_in(activity, ["object", "content"], "")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(object), do: {:ok, object}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
|
|||
|
|
@ -12,20 +12,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
|
|||
def history_awareness, do: :auto
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => type, "object" => child_object} = object)
|
||||
def filter(%{"type" => type, "object" => object} = activity)
|
||||
when type in ["Create", "Update"] do
|
||||
scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])
|
||||
|
||||
content =
|
||||
child_object["content"]
|
||||
object["content"]
|
||||
|> HTML.filter_tags(scrub_policy)
|
||||
|
||||
object = put_in(object, ["object", "content"], content)
|
||||
activity = put_in(activity, ["object", "content"], content)
|
||||
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
def filter(object), do: {:ok, object}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
|
|||
264
lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex
Normal file
264
lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
# 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.warning("""
|
||||
[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" => %{} = object} = activity) do
|
||||
case check_object_nsfw(object) do
|
||||
{:sfw, _} -> {:sfw, activity}
|
||||
{:nsfw, _} -> {:nsfw, activity}
|
||||
end
|
||||
end
|
||||
|
||||
def check_object_nsfw(object), do: {:sfw, object}
|
||||
|
||||
@impl true
|
||||
def filter(activity) do
|
||||
with {:sfw, activity} <- check_object_nsfw(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:nsfw, _data} -> handle_nsfw(activity)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_nsfw(activity) do
|
||||
if Config.get([@policy, :reject]) do
|
||||
{:reject, activity}
|
||||
else
|
||||
{:ok,
|
||||
activity
|
||||
|> maybe_unlist()
|
||||
|> maybe_mark_sensitive()}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_unlist(activity) do
|
||||
if Config.get([@policy, :unlist]) do
|
||||
unlist(activity)
|
||||
else
|
||||
activity
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_mark_sensitive(activity) do
|
||||
if Config.get([@policy, :mark_sensitive]) do
|
||||
mark_sensitive(activity)
|
||||
else
|
||||
activity
|
||||
end
|
||||
end
|
||||
|
||||
def unlist(%{"to" => to, "cc" => cc, "actor" => actor} = activity) 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()
|
||||
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
else
|
||||
_ -> raise "[NsfwApiPolicy]: Could not find user #{actor}"
|
||||
end
|
||||
end
|
||||
|
||||
def mark_sensitive(%{"object" => object} = activity) when is_map(object) do
|
||||
Map.put(activity, "object", mark_sensitive(object))
|
||||
end
|
||||
|
||||
def mark_sensitive(activity) when is_map(activity) do
|
||||
tags = (activity["tag"] || []) ++ ["nsfw"]
|
||||
|
||||
activity
|
||||
|> 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
|
||||
|
|
@ -11,12 +11,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
|||
@moduledoc "Filter activities depending on their age"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp check_date(%{"object" => %{"published" => published}} = message) do
|
||||
defp check_date(%{"object" => %{"published" => published}} = activity) do
|
||||
with %DateTime{} = now <- DateTime.utc_now(),
|
||||
{:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
|
||||
max_ttl <- Config.get([:mrf_object_age, :threshold]),
|
||||
{:ttl, false} <- {:ttl, DateTime.diff(now, then) > max_ttl} do
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
{:ttl, true} ->
|
||||
{:reject, nil}
|
||||
|
|
@ -26,73 +26,73 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
|||
end
|
||||
end
|
||||
|
||||
defp check_reject(message, actions) do
|
||||
defp check_reject(activity, actions) do
|
||||
if :reject in actions do
|
||||
{:reject, "[ObjectAgePolicy]"}
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_delist(message, actions) do
|
||||
defp check_delist(activity, actions) do
|
||||
if :delist in actions do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(activity["actor"]) do
|
||||
to =
|
||||
List.delete(message["to"] || [], Pleroma.Constants.as_public()) ++
|
||||
List.delete(activity["to"] || [], Pleroma.Constants.as_public()) ++
|
||||
[user.follower_address]
|
||||
|
||||
cc =
|
||||
List.delete(message["cc"] || [], user.follower_address) ++
|
||||
List.delete(activity["cc"] || [], user.follower_address) ++
|
||||
[Pleroma.Constants.as_public()]
|
||||
|
||||
message =
|
||||
message
|
||||
activity =
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|> Kernel.put_in(["object", "to"], to)
|
||||
|> Kernel.put_in(["object", "cc"], cc)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
_e ->
|
||||
{:reject, "[ObjectAgePolicy] Unhandled error"}
|
||||
end
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_strip_followers(message, actions) do
|
||||
defp check_strip_followers(activity, actions) do
|
||||
if :strip_followers in actions do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
|
||||
to = List.delete(message["to"] || [], user.follower_address)
|
||||
cc = List.delete(message["cc"] || [], user.follower_address)
|
||||
with %User{} = user <- User.get_cached_by_ap_id(activity["actor"]) do
|
||||
to = List.delete(activity["to"] || [], user.follower_address)
|
||||
cc = List.delete(activity["cc"] || [], user.follower_address)
|
||||
|
||||
message =
|
||||
message
|
||||
activity =
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|> Kernel.put_in(["object", "to"], to)
|
||||
|> Kernel.put_in(["object", "cc"], cc)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
_e ->
|
||||
{:reject, "[ObjectAgePolicy] Unhandled error"}
|
||||
end
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "object" => %{"published" => _}} = message) do
|
||||
def filter(%{"type" => "Create", "object" => %{"published" => _}} = activity) do
|
||||
with actions <- Config.get([:mrf_object_age, :actions]),
|
||||
{:reject, _} <- check_date(message),
|
||||
{:ok, message} <- check_reject(message, actions),
|
||||
{:ok, message} <- check_delist(message, actions),
|
||||
{:ok, message} <- check_strip_followers(message, actions) do
|
||||
{:ok, message}
|
||||
{:reject, _} <- check_date(activity),
|
||||
{:ok, activity} <- check_reject(activity, actions),
|
||||
{:ok, activity} <- check_delist(activity, actions),
|
||||
{:ok, activity} <- check_strip_followers(activity, actions) do
|
||||
{:ok, activity}
|
||||
else
|
||||
# check_date() is allowed to short-circuit the pipeline
|
||||
e -> e
|
||||
|
|
@ -100,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe do
|
||||
|
|
@ -131,8 +131,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
|||
type: {:list, :atom},
|
||||
description:
|
||||
"A list of actions to apply to the post. `:delist` removes the post from public timelines; " <>
|
||||
"`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines, additionally for followers-only it degrades to a direct message; " <>
|
||||
"`:reject` rejects the message entirely",
|
||||
"`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines, additionally for followers-only it degrades to a direct activity; " <>
|
||||
"`:reject` rejects the activity entirely",
|
||||
suggestions: [:delist, :strip_followers, :reject]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.Policy do
|
||||
@callback filter(map()) :: {:ok | :reject, map()}
|
||||
@callback filter(Pleroma.Activity.t()) :: {:ok | :reject, Pleroma.Activity.t()}
|
||||
@callback id_filter(String.t()) :: boolean()
|
||||
@callback describe() :: {:ok | :error, map()}
|
||||
@callback config_description() :: %{
|
||||
optional(:children) => [map()],
|
||||
|
|
@ -13,5 +14,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do
|
|||
description: String.t()
|
||||
}
|
||||
@callback history_awareness() :: :auto | :manual
|
||||
@optional_callbacks config_description: 0, history_awareness: 0
|
||||
@optional_callbacks config_description: 0, history_awareness: 0, id_filter: 1
|
||||
end
|
||||
|
|
|
|||
60
lib/pleroma/web/activity_pub/mrf/quiet_reply.ex
Normal file
60
lib/pleroma/web/activity_pub/mrf/quiet_reply.ex
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# 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.QuietReply do
|
||||
@moduledoc """
|
||||
QuietReply alters the scope of activities from local users when replying by enforcing them to be "Unlisted" or "Quiet Public". This delivers the activity to all the expected recipients and instances, but it will not be published in the Federated / The Whole Known Network timelines. It will still be published to the Home timelines of the user's followers and visible to anyone who opens the thread.
|
||||
"""
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def history_awareness, do: :auto
|
||||
|
||||
@impl true
|
||||
def filter(
|
||||
%{
|
||||
"type" => "Create",
|
||||
"to" => to,
|
||||
"cc" => cc,
|
||||
"object" => %{
|
||||
"actor" => actor,
|
||||
"type" => "Note",
|
||||
"inReplyTo" => in_reply_to
|
||||
}
|
||||
} = activity
|
||||
) do
|
||||
with true <- is_binary(in_reply_to),
|
||||
false <- match?([], cc),
|
||||
%User{follower_address: followers_collection, local: true} <-
|
||||
User.get_by_ap_id(actor) do
|
||||
updated_to =
|
||||
to
|
||||
|> Kernel.++([followers_collection])
|
||||
|> Kernel.--([Pleroma.Constants.as_public()])
|
||||
|
||||
updated_cc = [Pleroma.Constants.as_public()]
|
||||
|
||||
updated_activity =
|
||||
activity
|
||||
|> Map.put("to", updated_to)
|
||||
|> Map.put("cc", updated_cc)
|
||||
|> put_in(["object", "to"], updated_to)
|
||||
|> put_in(["object", "cc"], updated_cc)
|
||||
|
||||
{:ok, updated_activity}
|
||||
else
|
||||
_ -> {:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
end
|
||||
|
|
@ -10,18 +10,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do
|
|||
|
||||
require Pleroma.Constants
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@impl true
|
||||
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 true
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@impl true
|
||||
def history_awareness, do: :auto
|
||||
|
||||
defp filter_object(%{"quoteUrl" => quote_url} = object) do
|
||||
|
|
|
|||
118
lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex
Normal file
118
lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy do
|
||||
@moduledoc "Drop remote reports if they don't contain enough information."
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Flag"} = object) do
|
||||
with {_, false} <- {:local, local?(object)},
|
||||
{:ok, _} <- maybe_reject_all(object),
|
||||
{:ok, _} <- maybe_reject_anonymous(object),
|
||||
{:ok, _} <- maybe_reject_third_party(object),
|
||||
{:ok, _} <- maybe_reject_empty_message(object) do
|
||||
{:ok, object}
|
||||
else
|
||||
{:local, true} -> {:ok, object}
|
||||
{:reject, message} -> {:reject, message}
|
||||
error -> {:reject, error}
|
||||
end
|
||||
end
|
||||
|
||||
def filter(object), do: {:ok, object}
|
||||
|
||||
defp maybe_reject_all(object) do
|
||||
if Config.get([:mrf_remote_report, :reject_all]) do
|
||||
{:reject, "[RemoteReportPolicy] Remote report"}
|
||||
else
|
||||
{:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_reject_anonymous(%{"actor" => actor} = object) do
|
||||
with true <- Config.get([:mrf_remote_report, :reject_anonymous]),
|
||||
%URI{path: "/actor"} <- URI.parse(actor) do
|
||||
{:reject, "[RemoteReportPolicy] Anonymous: #{actor}"}
|
||||
else
|
||||
_ -> {:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_reject_third_party(%{"object" => objects} = object) do
|
||||
{_, to} =
|
||||
case objects do
|
||||
[head | tail] when is_binary(head) -> {tail, head}
|
||||
s when is_binary(s) -> {[], s}
|
||||
_ -> {[], ""}
|
||||
end
|
||||
|
||||
with true <- Config.get([:mrf_remote_report, :reject_third_party]),
|
||||
false <- String.starts_with?(to, Pleroma.Web.Endpoint.url()) do
|
||||
{:reject, "[RemoteReportPolicy] Third-party: #{to}"}
|
||||
else
|
||||
_ -> {:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_reject_empty_message(%{"content" => content} = object)
|
||||
when is_binary(content) and content != "" do
|
||||
{:ok, object}
|
||||
end
|
||||
|
||||
defp maybe_reject_empty_message(object) do
|
||||
if Config.get([:mrf_remote_report, :reject_empty_message]) do
|
||||
{:reject, ["RemoteReportPolicy] No content"]}
|
||||
else
|
||||
{:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
defp local?(%{"actor" => actor}) do
|
||||
String.starts_with?(actor, Pleroma.Web.Endpoint.url())
|
||||
end
|
||||
|
||||
@impl true
|
||||
def describe do
|
||||
mrf_remote_report =
|
||||
Config.get(:mrf_remote_report)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{:ok, %{mrf_remote_report: mrf_remote_report}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def config_description do
|
||||
%{
|
||||
key: :mrf_remote_report,
|
||||
related_policy: "Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy",
|
||||
label: "MRF Remote Report",
|
||||
description: "Drop remote reports if they don't contain enough information.",
|
||||
children: [
|
||||
%{
|
||||
key: :reject_all,
|
||||
type: :boolean,
|
||||
description: "Reject all remote reports? (this option takes precedence)",
|
||||
suggestions: [false]
|
||||
},
|
||||
%{
|
||||
key: :reject_anonymous,
|
||||
type: :boolean,
|
||||
description: "Reject anonymous remote reports?",
|
||||
suggestions: [true]
|
||||
},
|
||||
%{
|
||||
key: :reject_third_party,
|
||||
type: :boolean,
|
||||
description: "Reject reports on users from third-party instances?",
|
||||
suggestions: [true]
|
||||
},
|
||||
%{
|
||||
key: :reject_empty_message,
|
||||
type: :boolean,
|
||||
description: "Reject remote reports with no message?",
|
||||
suggestions: [true]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -13,20 +13,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
require Pleroma.Constants
|
||||
|
||||
defp check_accept(%{host: actor_host} = _actor_info, object) do
|
||||
defp check_accept(%{host: actor_host} = _actor_info, activity) do
|
||||
accepts =
|
||||
instance_list(:accept)
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
cond do
|
||||
accepts == [] -> {:ok, object}
|
||||
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
|
||||
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
|
||||
accepts == [] -> {:ok, activity}
|
||||
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, activity}
|
||||
MRF.subdomain_match?(accepts, actor_host) -> {:ok, activity}
|
||||
true -> {:reject, "[SimplePolicy] host not in accept list"}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_reject(%{host: actor_host} = _actor_info, object) do
|
||||
defp check_reject(%{host: actor_host} = _actor_info, activity) do
|
||||
rejects =
|
||||
instance_list(:reject)
|
||||
|> MRF.subdomains_regex()
|
||||
|
|
@ -34,109 +34,109 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
if MRF.subdomain_match?(rejects, actor_host) do
|
||||
{:reject, "[SimplePolicy] host in reject list"}
|
||||
else
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_media_removal(
|
||||
%{host: actor_host} = _actor_info,
|
||||
%{"type" => type, "object" => %{"attachment" => child_attachment}} = object
|
||||
%{"type" => type, "object" => %{"attachment" => object_attachment}} = activity
|
||||
)
|
||||
when length(child_attachment) > 0 and type in ["Create", "Update"] do
|
||||
when length(object_attachment) > 0 and type in ["Create", "Update"] do
|
||||
media_removal =
|
||||
instance_list(:media_removal)
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
object =
|
||||
activity =
|
||||
if MRF.subdomain_match?(media_removal, actor_host) do
|
||||
child_object = Map.delete(object["object"], "attachment")
|
||||
Map.put(object, "object", child_object)
|
||||
object = Map.delete(activity["object"], "attachment")
|
||||
Map.put(activity, "object", object)
|
||||
else
|
||||
object
|
||||
activity
|
||||
end
|
||||
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
defp check_media_removal(_actor_info, object), do: {:ok, object}
|
||||
defp check_media_removal(_actor_info, activity), do: {:ok, activity}
|
||||
|
||||
defp check_media_nsfw(
|
||||
%{host: actor_host} = _actor_info,
|
||||
%{
|
||||
"type" => type,
|
||||
"object" => %{} = _child_object
|
||||
} = object
|
||||
"object" => %{} = _object
|
||||
} = activity
|
||||
)
|
||||
when type in ["Create", "Update"] do
|
||||
media_nsfw =
|
||||
instance_list(:media_nsfw)
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
object =
|
||||
activity =
|
||||
if MRF.subdomain_match?(media_nsfw, actor_host) do
|
||||
Kernel.put_in(object, ["object", "sensitive"], true)
|
||||
Kernel.put_in(activity, ["object", "sensitive"], true)
|
||||
else
|
||||
object
|
||||
activity
|
||||
end
|
||||
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
defp check_media_nsfw(_actor_info, object), do: {:ok, object}
|
||||
defp check_media_nsfw(_actor_info, activity), do: {:ok, activity}
|
||||
|
||||
defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
|
||||
defp check_ftl_removal(%{host: actor_host} = _actor_info, activity) do
|
||||
timeline_removal =
|
||||
instance_list(:federated_timeline_removal)
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
object =
|
||||
activity =
|
||||
with true <- MRF.subdomain_match?(timeline_removal, actor_host),
|
||||
user <- User.get_cached_by_ap_id(object["actor"]),
|
||||
true <- Pleroma.Constants.as_public() in object["to"] do
|
||||
to = List.delete(object["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
|
||||
user <- User.get_cached_by_ap_id(activity["actor"]),
|
||||
true <- Pleroma.Constants.as_public() in activity["to"] do
|
||||
to = List.delete(activity["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
|
||||
|
||||
cc = List.delete(object["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
|
||||
cc = List.delete(activity["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
|
||||
|
||||
object
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
else
|
||||
_ -> object
|
||||
_ -> activity
|
||||
end
|
||||
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
defp intersection(list1, list2) do
|
||||
list1 -- list1 -- list2
|
||||
end
|
||||
|
||||
defp check_followers_only(%{host: actor_host} = _actor_info, object) do
|
||||
defp check_followers_only(%{host: actor_host} = _actor_info, activity) do
|
||||
followers_only =
|
||||
instance_list(:followers_only)
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
object =
|
||||
activity =
|
||||
with true <- MRF.subdomain_match?(followers_only, actor_host),
|
||||
user <- User.get_cached_by_ap_id(object["actor"]) do
|
||||
user <- User.get_cached_by_ap_id(activity["actor"]) do
|
||||
# Don't use Map.get/3 intentionally, these must not be nil
|
||||
fixed_to = object["to"] || []
|
||||
fixed_cc = object["cc"] || []
|
||||
fixed_to = activity["to"] || []
|
||||
fixed_cc = activity["cc"] || []
|
||||
|
||||
to = FollowingRelationship.followers_ap_ids(user, fixed_to)
|
||||
cc = FollowingRelationship.followers_ap_ids(user, fixed_cc)
|
||||
|
||||
object
|
||||
activity
|
||||
|> Map.put("to", intersection([user.follower_address | to], fixed_to))
|
||||
|> Map.put("cc", intersection([user.follower_address | cc], fixed_cc))
|
||||
else
|
||||
_ -> object
|
||||
_ -> activity
|
||||
end
|
||||
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
|
||||
defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = activity) do
|
||||
report_removal =
|
||||
instance_list(:report_removal)
|
||||
|> MRF.subdomains_regex()
|
||||
|
|
@ -144,39 +144,39 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
if MRF.subdomain_match?(report_removal, actor_host) do
|
||||
{:reject, "[SimplePolicy] host in report_removal list"}
|
||||
else
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_report_removal(_actor_info, object), do: {:ok, object}
|
||||
defp check_report_removal(_actor_info, activity), do: {:ok, activity}
|
||||
|
||||
defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
|
||||
defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = activity) do
|
||||
avatar_removal =
|
||||
instance_list(:avatar_removal)
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(avatar_removal, actor_host) do
|
||||
{:ok, Map.delete(object, "icon")}
|
||||
{:ok, Map.delete(activity, "icon")}
|
||||
else
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_avatar_removal(_actor_info, object), do: {:ok, object}
|
||||
defp check_avatar_removal(_actor_info, activity), do: {:ok, activity}
|
||||
|
||||
defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
|
||||
defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = activity) do
|
||||
banner_removal =
|
||||
instance_list(:banner_removal)
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(banner_removal, actor_host) do
|
||||
{:ok, Map.delete(object, "image")}
|
||||
{:ok, Map.delete(activity, "image")}
|
||||
else
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_banner_removal(_actor_info, object), do: {:ok, object}
|
||||
defp check_banner_removal(_actor_info, activity), do: {:ok, activity}
|
||||
|
||||
defp check_object(%{"object" => object} = activity) do
|
||||
with {:ok, _object} <- filter(object) do
|
||||
|
|
@ -184,7 +184,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
end
|
||||
end
|
||||
|
||||
defp check_object(object), do: {:ok, object}
|
||||
defp check_object(activity), do: {:ok, activity}
|
||||
|
||||
defp instance_list(config_key) do
|
||||
Config.get([:mrf_simple, config_key])
|
||||
|
|
@ -192,7 +192,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Delete", "actor" => actor} = object) do
|
||||
def id_filter(id) do
|
||||
host_info = URI.parse(id)
|
||||
|
||||
with {:ok, _} <- check_accept(host_info, %{}),
|
||||
{:ok, _} <- check_reject(host_info, %{}) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Delete", "actor" => actor} = activity) do
|
||||
%{host: actor_host} = URI.parse(actor)
|
||||
|
||||
reject_deletes =
|
||||
|
|
@ -202,60 +214,54 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
if MRF.subdomain_match?(reject_deletes, actor_host) do
|
||||
{:reject, "[SimplePolicy] host in reject_deletes list"}
|
||||
else
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"actor" => actor} = object) do
|
||||
def filter(%{"actor" => actor} = activity) do
|
||||
actor_info = URI.parse(actor)
|
||||
|
||||
with {:ok, object} <- check_accept(actor_info, object),
|
||||
{:ok, object} <- check_reject(actor_info, object),
|
||||
{:ok, object} <- check_media_removal(actor_info, object),
|
||||
{:ok, object} <- check_media_nsfw(actor_info, object),
|
||||
{:ok, object} <- check_ftl_removal(actor_info, object),
|
||||
{:ok, object} <- check_followers_only(actor_info, object),
|
||||
{:ok, object} <- check_report_removal(actor_info, object),
|
||||
{:ok, object} <- check_object(object) do
|
||||
{:ok, object}
|
||||
with {:ok, activity} <- check_accept(actor_info, activity),
|
||||
{:ok, activity} <- check_reject(actor_info, activity),
|
||||
{:ok, activity} <- check_media_removal(actor_info, activity),
|
||||
{:ok, activity} <- check_media_nsfw(actor_info, activity),
|
||||
{:ok, activity} <- check_ftl_removal(actor_info, activity),
|
||||
{:ok, activity} <- check_followers_only(actor_info, activity),
|
||||
{:ok, activity} <- check_report_removal(actor_info, activity),
|
||||
{:ok, activity} <- check_object(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||
{:reject, _} = e -> e
|
||||
_ -> {:reject, "[SimplePolicy]"}
|
||||
end
|
||||
end
|
||||
|
||||
def filter(%{"id" => actor, "type" => obj_type} = object)
|
||||
when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do
|
||||
def filter(%{"id" => actor, "type" => actor_type} = activity)
|
||||
when actor_type in ["Application", "Group", "Organization", "Person", "Service"] do
|
||||
actor_info = URI.parse(actor)
|
||||
|
||||
with {:ok, object} <- check_accept(actor_info, object),
|
||||
{:ok, object} <- check_reject(actor_info, object),
|
||||
{:ok, object} <- check_avatar_removal(actor_info, object),
|
||||
{:ok, object} <- check_banner_removal(actor_info, object) do
|
||||
{:ok, object}
|
||||
with {:ok, activity} <- check_accept(actor_info, activity),
|
||||
{:ok, activity} <- check_reject(actor_info, activity),
|
||||
{:ok, activity} <- check_avatar_removal(actor_info, activity),
|
||||
{:ok, activity} <- check_banner_removal(actor_info, activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||
{:reject, _} = e -> e
|
||||
_ -> {:reject, "[SimplePolicy]"}
|
||||
end
|
||||
end
|
||||
|
||||
def filter(object) when is_binary(object) do
|
||||
uri = URI.parse(object)
|
||||
def filter(activity) when is_binary(activity) do
|
||||
uri = URI.parse(activity)
|
||||
|
||||
with {:ok, object} <- check_accept(uri, object),
|
||||
{:ok, object} <- check_reject(uri, object) do
|
||||
{:ok, object}
|
||||
with {:ok, activity} <- check_accept(uri, activity),
|
||||
{:ok, activity} <- check_reject(uri, activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||
{:reject, _} = e -> e
|
||||
_ -> {:reject, "[SimplePolicy]"}
|
||||
end
|
||||
end
|
||||
|
||||
def filter(object), do: {:ok, object}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe do
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do
|
||||
def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = activity) do
|
||||
host = URI.parse(actor).host
|
||||
|
||||
if host != Pleroma.Web.Endpoint.host() and accept_host?(host) do
|
||||
|
|
@ -97,10 +97,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
|||
end
|
||||
end
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
@spec config_description :: %{
|
||||
|
|
|
|||
|
|
@ -20,20 +20,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"actor" => actor} = message) do
|
||||
def filter(%{"actor" => actor} = activity) do
|
||||
with {:ok, match, subchain} <- lookup_subchain(actor) do
|
||||
Logger.debug(
|
||||
"[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{inspect(subchain)}"
|
||||
)
|
||||
|
||||
MRF.filter(subchain, message)
|
||||
MRF.filter(subchain, activity)
|
||||
else
|
||||
_e -> {:ok, message}
|
||||
_e -> {:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
@ -45,7 +45,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
|
|||
related_policy: "Pleroma.Web.ActivityPub.MRF.SubchainPolicy",
|
||||
label: "MRF Subchain",
|
||||
description:
|
||||
"This policy processes messages through an alternate pipeline when a given message matches certain criteria." <>
|
||||
"This policy processes activities through an alternate pipeline when a given activity matches certain criteria." <>
|
||||
" All criteria are configured as a map of regular expressions to lists of policy modules.",
|
||||
children: [
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -28,25 +28,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
"mrf_tag:media-force-nsfw",
|
||||
%{
|
||||
"type" => type,
|
||||
"object" => %{"attachment" => child_attachment}
|
||||
} = message
|
||||
"object" => %{"attachment" => object_attachment}
|
||||
} = activity
|
||||
)
|
||||
when length(child_attachment) > 0 and type in ["Create", "Update"] do
|
||||
{:ok, Kernel.put_in(message, ["object", "sensitive"], true)}
|
||||
when length(object_attachment) > 0 and type in ["Create", "Update"] do
|
||||
{:ok, Kernel.put_in(activity, ["object", "sensitive"], true)}
|
||||
end
|
||||
|
||||
defp process_tag(
|
||||
"mrf_tag:media-strip",
|
||||
%{
|
||||
"type" => type,
|
||||
"object" => %{"attachment" => child_attachment} = object
|
||||
} = message
|
||||
"object" => %{"attachment" => object_attachment} = object
|
||||
} = activity
|
||||
)
|
||||
when length(child_attachment) > 0 and type in ["Create", "Update"] do
|
||||
when length(object_attachment) > 0 and type in ["Create", "Update"] do
|
||||
object = Map.delete(object, "attachment")
|
||||
message = Map.put(message, "object", object)
|
||||
activity = Map.put(activity, "object", object)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
defp process_tag(
|
||||
|
|
@ -57,7 +57,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
"cc" => cc,
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
} = message
|
||||
} = activity
|
||||
) do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
|
||||
|
|
@ -70,15 +70,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|
||||
message =
|
||||
message
|
||||
activity =
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|> Map.put("object", object)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
"cc" => cc,
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
} = message
|
||||
} = activity
|
||||
) do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
|
||||
|
|
@ -104,26 +104,26 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|
||||
message =
|
||||
message
|
||||
activity =
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|> Map.put("object", object)
|
||||
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_tag(
|
||||
"mrf_tag:disable-remote-subscription",
|
||||
%{"type" => "Follow", "actor" => actor} = message
|
||||
%{"type" => "Follow", "actor" => actor} = activity
|
||||
) do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
|
||||
if user.local == true do
|
||||
{:ok, message}
|
||||
{:ok, activity}
|
||||
else
|
||||
{:reject,
|
||||
"[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"}
|
||||
|
|
@ -133,14 +133,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow", "actor" => actor}),
|
||||
do: {:reject, "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-any-subscription"}
|
||||
|
||||
defp process_tag(_, message), do: {:ok, message}
|
||||
defp process_tag(_, activity), do: {:ok, activity}
|
||||
|
||||
def filter_message(actor, message) do
|
||||
def filter_activity(actor, activity) do
|
||||
User.get_cached_by_ap_id(actor)
|
||||
|> get_tags()
|
||||
|> Enum.reduce({:ok, message}, fn
|
||||
tag, {:ok, message} ->
|
||||
process_tag(tag, message)
|
||||
|> Enum.reduce({:ok, activity}, fn
|
||||
tag, {:ok, activity} ->
|
||||
process_tag(tag, activity)
|
||||
|
||||
_, error ->
|
||||
error
|
||||
|
|
@ -148,15 +148,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"object" => target_actor, "type" => "Follow"} = message),
|
||||
do: filter_message(target_actor, message)
|
||||
def filter(%{"object" => target_actor, "type" => "Follow"} = activity),
|
||||
do: filter_activity(target_actor, activity)
|
||||
|
||||
@impl true
|
||||
def filter(%{"actor" => actor, "type" => type} = message) when type in ["Create", "Update"],
|
||||
do: filter_message(actor, message)
|
||||
def filter(%{"actor" => actor, "type" => type} = activity) when type in ["Create", "Update"],
|
||||
do: filter_activity(actor, activity)
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
|
|||
@moduledoc "Accept-list of users from specified instances"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp filter_by_list(object, []), do: {:ok, object}
|
||||
defp filter_by_list(activity, []), do: {:ok, activity}
|
||||
|
||||
defp filter_by_list(%{"actor" => actor} = object, allow_list) do
|
||||
defp filter_by_list(%{"actor" => actor} = activity, allow_list) do
|
||||
if actor in allow_list do
|
||||
{:ok, object}
|
||||
{:ok, activity}
|
||||
else
|
||||
{:reject, "[UserAllowListPolicy] #{actor} not in the list"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"actor" => actor} = object) do
|
||||
def filter(%{"actor" => actor} = activity) do
|
||||
actor_info = URI.parse(actor)
|
||||
|
||||
allow_list =
|
||||
|
|
@ -28,10 +28,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
|
|||
[]
|
||||
)
|
||||
|
||||
filter_by_list(object, allow_list)
|
||||
filter_by_list(activity, allow_list)
|
||||
end
|
||||
|
||||
def filter(object), do: {:ok, object}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe do
|
||||
|
|
|
|||
|
|
@ -3,39 +3,38 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
|
||||
@moduledoc "Filter messages which belong to certain activity vocabularies"
|
||||
@moduledoc "Filter activities which belong to certain activity vocabularies"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Undo", "object" => child_message} = message) do
|
||||
with {:ok, _} <- filter(child_message) do
|
||||
{:ok, message}
|
||||
def filter(%{"type" => "Undo", "object" => object} = activity) do
|
||||
with {:ok, _} <- filter(object) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:reject, _} = e -> e
|
||||
end
|
||||
end
|
||||
|
||||
def filter(%{"type" => message_type} = message) do
|
||||
def filter(%{"type" => activity_type} = activity) do
|
||||
with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),
|
||||
rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]),
|
||||
{_, true} <-
|
||||
{:accepted,
|
||||
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type)},
|
||||
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, activity_type)},
|
||||
{_, false} <-
|
||||
{:rejected,
|
||||
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type)},
|
||||
{:ok, _} <- filter(message["object"]) do
|
||||
{:ok, message}
|
||||
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, activity_type)},
|
||||
{:ok, _} <- filter(activity["object"]) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:reject, _} = e -> e
|
||||
{:accepted, _} -> {:reject, "[VocabularyPolicy] #{message_type} not in accept list"}
|
||||
{:rejected, _} -> {:reject, "[VocabularyPolicy] #{message_type} in reject list"}
|
||||
_ -> {:reject, "[VocabularyPolicy]"}
|
||||
{:accepted, _} -> {:reject, "[VocabularyPolicy] #{activity_type} not in accept list"}
|
||||
{:rejected, _} -> {:reject, "[VocabularyPolicy] #{activity_type} in reject list"}
|
||||
end
|
||||
end
|
||||
|
||||
def filter(message), do: {:ok, message}
|
||||
def filter(activity), do: {:ok, activity}
|
||||
|
||||
@impl true
|
||||
def describe,
|
||||
|
|
@ -47,20 +46,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
|
|||
key: :mrf_vocabulary,
|
||||
related_policy: "Pleroma.Web.ActivityPub.MRF.VocabularyPolicy",
|
||||
label: "MRF Vocabulary",
|
||||
description: "Filter messages which belong to certain activity vocabularies",
|
||||
description: "Filter activities which belong to certain activity vocabularies",
|
||||
children: [
|
||||
%{
|
||||
key: :accept,
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"A list of ActivityStreams terms to accept. If empty, all supported messages are accepted.",
|
||||
"A list of ActivityStreams terms to accept. If empty, all supported activities are accepted.",
|
||||
suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
|
||||
},
|
||||
%{
|
||||
key: :reject,
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"A list of ActivityStreams terms to reject. If empty, no messages are rejected.",
|
||||
"A list of ActivityStreams terms to reject. If empty, no activities are rejected.",
|
||||
suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
|
||||
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
|
||||
|
||||
import Pleroma.Constants, only: [activity_types: 0, object_types: 0]
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Object
|
||||
|
|
@ -39,6 +41,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
@impl true
|
||||
def validate(object, meta)
|
||||
|
||||
# This overload works together with the InboxGuardPlug
|
||||
# and ensures that we are not accepting any activity type
|
||||
# that cannot pass InboxGuardPlug.
|
||||
# If we want to support any more activity types, make sure to
|
||||
# add it in Pleroma.Constants's activity_types or object_types,
|
||||
# and, if applicable, allowed_activity_types_from_strangers.
|
||||
def validate(%{"type" => type}, _meta)
|
||||
when type not in activity_types() and type not in object_types(),
|
||||
do: {:error, :not_allowed_object_type}
|
||||
|
||||
def validate(%{"type" => "Block"} = block_activity, meta) do
|
||||
with {:ok, block_activity} <-
|
||||
block_activity
|
||||
|
|
@ -165,7 +177,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
meta = Keyword.put(meta, :object_data, object_data),
|
||||
{:ok, update_activity} <-
|
||||
update_activity
|
||||
|> UpdateValidator.cast_and_validate()
|
||||
|> UpdateValidator.cast_and_validate(meta)
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
update_activity = stringify_keys(update_activity)
|
||||
{:ok, update_activity, meta}
|
||||
|
|
@ -173,7 +185,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
{:local, _} ->
|
||||
with {:ok, object} <-
|
||||
update_activity
|
||||
|> UpdateValidator.cast_and_validate()
|
||||
|> UpdateValidator.cast_and_validate(meta)
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object)
|
||||
{:ok, object, meta}
|
||||
|
|
@ -203,9 +215,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
"Answer" -> AnswerValidator
|
||||
end
|
||||
|
||||
cast_func =
|
||||
if type == "Update" do
|
||||
fn o -> validator.cast_and_validate(o, meta) end
|
||||
else
|
||||
fn o -> validator.cast_and_validate(o) end
|
||||
end
|
||||
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|> validator.cast_and_validate()
|
||||
|> cast_func.()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object)
|
||||
{:ok, object, meta}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do
|
|||
|
||||
defp validate_data(cng) do
|
||||
cng
|
||||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_required([:id, :type, :actor, :to, :object])
|
||||
|> validate_inclusion(:type, ["Accept", "Reject"])
|
||||
|> validate_actor_presence()
|
||||
|> validate_object_presence(allowed_types: ["Follow"])
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
|> fix_replies()
|
||||
|> fix_attachments()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> CommonFixes.fix_likes()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> Transmogrifier.fix_content_map()
|
||||
|> CommonFixes.maybe_add_language()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|
|||
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
|
||||
|
|
@ -44,7 +45,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|
|||
|> fix_url()
|
||||
|
||||
struct
|
||||
|> cast(data, [:id, :type, :mediaType, :name, :blurhash])
|
||||
|> cast(data, [:id, :type, :mediaType, :name, :summary, :blurhash])
|
||||
|> cast_embed(:url, with: &url_changeset/2, required: true)
|
||||
|> validate_inclusion(:type, ~w[Link Document Audio Image Video])
|
||||
|> validate_required([:type, :mediaType])
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do
|
|||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_object_defaults()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> CommonFixes.fix_likes()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> fix_url()
|
||||
|> fix_content()
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do
|
|||
|
||||
defp validate_data(cng) do
|
||||
cng
|
||||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_required([:id, :type, :actor, :to, :object])
|
||||
|> validate_inclusion(:type, ["Block"])
|
||||
|> CommonValidations.validate_actor_presence()
|
||||
|> CommonValidations.validate_actor_presence(field_name: :object)
|
||||
|
|
|
|||
|
|
@ -120,6 +120,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
|
||||
def fix_quote_url(data), do: data
|
||||
|
||||
# On Mastodon, `"likes"` attribute includes an inlined `Collection` with `totalItems`,
|
||||
# not a list of users.
|
||||
# https://github.com/mastodon/mastodon/pull/32007
|
||||
def fix_likes(%{"likes" => %{}} = data), do: Map.drop(data, ["likes"])
|
||||
|
||||
def fix_likes(data), do: data
|
||||
|
||||
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
|
||||
def object_link_tag?(%{
|
||||
"type" => "Link",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
|
|||
data
|
||||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_object_defaults()
|
||||
|> CommonFixes.fix_likes()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> CommonFixes.maybe_add_language()
|
||||
|> CommonFixes.maybe_add_content_map()
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do
|
|||
|
||||
defp validate_data(cng) do
|
||||
cng
|
||||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_required([:id, :type, :actor, :to, :object])
|
||||
|> validate_inclusion(:type, ["Follow"])
|
||||
|> validate_inclusion(:state, ~w{pending reject accept})
|
||||
|> validate_actor_presence()
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|
|||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_object_defaults()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> CommonFixes.fix_likes()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> fix_closed()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
|
|||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
|
@ -31,23 +33,50 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
|
|||
|> cast(data, __schema__(:fields))
|
||||
end
|
||||
|
||||
defp validate_data(cng) do
|
||||
defp validate_data(cng, meta) do
|
||||
cng
|
||||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_inclusion(:type, ["Update"])
|
||||
|> validate_actor_presence()
|
||||
|> validate_updating_rights()
|
||||
|> validate_updating_rights(meta)
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
def cast_and_validate(data, meta \\ []) do
|
||||
data
|
||||
|> cast_data
|
||||
|> validate_data
|
||||
|> validate_data(meta)
|
||||
end
|
||||
|
||||
# For now we only support updating users, and here the rule is easy:
|
||||
# object id == actor id
|
||||
def validate_updating_rights(cng) do
|
||||
def validate_updating_rights(cng, meta) do
|
||||
if meta[:local] do
|
||||
validate_updating_rights_local(cng)
|
||||
else
|
||||
validate_updating_rights_remote(cng)
|
||||
end
|
||||
end
|
||||
|
||||
# For local Updates, verify the actor can edit the object
|
||||
def validate_updating_rights_local(cng) do
|
||||
actor = get_field(cng, :actor)
|
||||
updated_object = get_field(cng, :object)
|
||||
|
||||
if {:ok, actor} == ObjectValidators.ObjectID.cast(updated_object) do
|
||||
cng
|
||||
else
|
||||
with %User{} = user <- User.get_cached_by_ap_id(actor),
|
||||
{_, %Object{} = orig_object} <- {:object, Object.normalize(updated_object)},
|
||||
:ok <- Object.authorize_access(orig_object, user) do
|
||||
cng
|
||||
else
|
||||
_e ->
|
||||
cng
|
||||
|> add_error(:object, "Can't be updated by this actor")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# For remote Updates, verify the host is the same.
|
||||
def validate_updating_rights_remote(cng) do
|
||||
with actor = get_field(cng, :actor),
|
||||
object = get_field(cng, :object),
|
||||
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
|
||||
|
|
|
|||
|
|
@ -22,22 +22,27 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
|
|||
defp activity_pub, do: Config.get([:pipeline, :activity_pub], ActivityPub)
|
||||
defp config, do: Config.get([:pipeline, :config], Config)
|
||||
|
||||
@spec common_pipeline(map(), keyword()) ::
|
||||
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
|
||||
@type results :: {:ok, Activity.t() | Object.t(), keyword()}
|
||||
@type errors :: {:error | :reject, any()}
|
||||
|
||||
# The Repo.transaction will wrap the result in an {:ok, _}
|
||||
# and only returns an {:error, _} if the error encountered was related
|
||||
# to the SQL transaction
|
||||
@spec common_pipeline(map(), keyword()) :: results() | errors()
|
||||
def common_pipeline(object, meta) do
|
||||
case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do
|
||||
{:ok, {:ok, activity, meta}} ->
|
||||
side_effects().handle_after_transaction(meta)
|
||||
{:ok, activity, meta}
|
||||
|
||||
{:ok, value} ->
|
||||
value
|
||||
{:ok, {:error, _} = error} ->
|
||||
error
|
||||
|
||||
{:ok, {:reject, _} = error} ->
|
||||
error
|
||||
|
||||
{:error, e} ->
|
||||
{:error, e}
|
||||
|
||||
{:reject, e} ->
|
||||
{:reject, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Publisher.Prepared
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Workers.PublisherWorker
|
||||
|
|
@ -30,11 +31,11 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
"""
|
||||
@spec enqueue_one(map(), Keyword.t()) :: {:ok, %Oban.Job{}}
|
||||
def enqueue_one(%{} = params, worker_args \\ []) do
|
||||
PublisherWorker.enqueue(
|
||||
"publish_one",
|
||||
%{"params" => params},
|
||||
PublisherWorker.new(
|
||||
%{"op" => "publish_one", "params" => params},
|
||||
worker_args
|
||||
)
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -76,17 +77,29 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Publish a single message to a peer. Takes a struct with the following
|
||||
parameters set:
|
||||
|
||||
Prepare an activity for publishing from an Oban job
|
||||
* `inbox`: the inbox to publish to
|
||||
* `json`: the JSON message body representing the ActivityPub message
|
||||
* `actor`: the actor which is signing the message
|
||||
* `id`: the ActivityStreams URI of the message
|
||||
* `activity_id`: the internal activity id
|
||||
* `cc`: the cc recipients relevant to this inbox (optional)
|
||||
"""
|
||||
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
|
||||
Logger.debug("Federating #{id} to #{inbox}")
|
||||
@spec prepare_one(map()) :: Prepared.t()
|
||||
def prepare_one(%{inbox: inbox, activity_id: activity_id} = params) do
|
||||
activity = Activity.get_by_id_with_user_actor(activity_id)
|
||||
actor = activity.user_actor
|
||||
|
||||
ap_id = activity.data["id"]
|
||||
Logger.debug("Federating #{ap_id} to #{inbox}")
|
||||
uri = %{path: path} = URI.parse(inbox)
|
||||
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
|
||||
cc = Map.get(params, :cc, [])
|
||||
|
||||
json =
|
||||
data
|
||||
|> Map.put("cc", cc)
|
||||
|> Jason.encode!()
|
||||
|
||||
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
|
||||
|
||||
date = Pleroma.Signature.signed_date()
|
||||
|
|
@ -100,55 +113,83 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
date: date
|
||||
})
|
||||
|
||||
%Prepared{
|
||||
activity_id: activity_id,
|
||||
json: json,
|
||||
date: date,
|
||||
signature: signature,
|
||||
digest: digest,
|
||||
inbox: inbox,
|
||||
unreachable_since: params[:unreachable_since]
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Publish a single message to a peer. Takes a struct with the following
|
||||
parameters set:
|
||||
* `activity_id`: the activity id
|
||||
* `json`: the json payload
|
||||
* `date`: the signed date from Pleroma.Signature.signed_date()
|
||||
* `signature`: the signature from Pleroma.Signature.sign/2
|
||||
* `digest`: base64 encoded the hash of the json payload prefixed with "SHA-256="
|
||||
* `inbox`: the inbox URI of this delivery
|
||||
* `unreachable_since`: timestamp the instance was marked unreachable
|
||||
|
||||
"""
|
||||
def publish_one(%Prepared{} = p) do
|
||||
with {:ok, %{status: code}} = result when code in 200..299 <-
|
||||
HTTP.post(
|
||||
inbox,
|
||||
json,
|
||||
p.inbox,
|
||||
p.json,
|
||||
[
|
||||
{"Content-Type", "application/activity+json"},
|
||||
{"Date", date},
|
||||
{"signature", signature},
|
||||
{"digest", digest}
|
||||
{"Date", p.date},
|
||||
{"signature", p.signature},
|
||||
{"digest", p.digest}
|
||||
]
|
||||
) do
|
||||
if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
|
||||
Instances.set_reachable(inbox)
|
||||
if not is_nil(p.unreachable_since) do
|
||||
Instances.set_reachable(p.inbox)
|
||||
end
|
||||
|
||||
result
|
||||
else
|
||||
{_post_result, %{status: code} = response} = e ->
|
||||
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
|
||||
Logger.metadata(activity: id, inbox: inbox, status: code)
|
||||
Logger.error("Publisher failed to inbox #{inbox} with status #{code}")
|
||||
if is_nil(p.unreachable_since) do
|
||||
Instances.set_unreachable(p.inbox)
|
||||
end
|
||||
|
||||
Logger.metadata(activity: p.activity_id, inbox: p.inbox, status: code)
|
||||
Logger.error("Publisher failed to inbox #{p.inbox} with status #{code}")
|
||||
|
||||
case response do
|
||||
%{status: 403} -> {:discard, :forbidden}
|
||||
%{status: 404} -> {:discard, :not_found}
|
||||
%{status: 410} -> {:discard, :not_found}
|
||||
%{status: 400} -> {:cancel, :bad_request}
|
||||
%{status: 403} -> {:cancel, :forbidden}
|
||||
%{status: 404} -> {:cancel, :not_found}
|
||||
%{status: 410} -> {:cancel, :not_found}
|
||||
_ -> {:error, e}
|
||||
end
|
||||
|
||||
{:error, {:already_started, _}} ->
|
||||
Logger.debug("Publisher snoozing worker job due worker :already_started race condition")
|
||||
connection_pool_snooze()
|
||||
|
||||
{:error, :pool_full} ->
|
||||
Logger.debug("Publisher snoozing worker job due to full connection pool")
|
||||
{:snooze, 30}
|
||||
connection_pool_snooze()
|
||||
|
||||
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)}")
|
||||
if is_nil(p.unreachable_since) do
|
||||
Instances.set_unreachable(p.inbox)
|
||||
end
|
||||
|
||||
Logger.metadata(activity: p.activity_id, inbox: p.inbox)
|
||||
Logger.error("Publisher failed to inbox #{p.inbox} #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def publish_one(%{actor_id: actor_id} = params) do
|
||||
actor = User.get_cached_by_id(actor_id)
|
||||
|
||||
params
|
||||
|> Map.delete(:actor_id)
|
||||
|> Map.put(:actor, actor)
|
||||
|> publish_one()
|
||||
end
|
||||
defp connection_pool_snooze, do: {:snooze, 3}
|
||||
|
||||
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
|
||||
if port == URI.default_port(scheme) do
|
||||
|
|
@ -250,7 +291,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
|
||||
when is_list(bcc) and bcc != [] do
|
||||
public = public?(activity)
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
|
||||
[priority_recipients, recipients] = recipients(actor, activity)
|
||||
|
||||
|
|
@ -275,16 +315,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
# 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!()
|
||||
|
||||
__MODULE__.enqueue_one(%{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor_id: actor.id,
|
||||
id: activity.data["id"],
|
||||
cc: cc,
|
||||
activity_id: activity.id,
|
||||
unreachable_since: unreachable_since
|
||||
})
|
||||
end)
|
||||
|
|
@ -301,9 +335,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
Relay.publish(activity)
|
||||
end
|
||||
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
json = Jason.encode!(data)
|
||||
|
||||
[priority_inboxes, inboxes] =
|
||||
recipients(actor, activity)
|
||||
|> Enum.map(fn recipients ->
|
||||
|
|
@ -325,9 +356,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
__MODULE__.enqueue_one(
|
||||
%{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor_id: actor.id,
|
||||
id: activity.data["id"],
|
||||
activity_id: activity.id,
|
||||
unreachable_since: unreachable_since
|
||||
},
|
||||
priority: priority
|
||||
|
|
|
|||
8
lib/pleroma/web/activity_pub/publisher/prepared.ex
Normal file
8
lib/pleroma/web/activity_pub/publisher/prepared.ex
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.Publisher.Prepared do
|
||||
@type t :: %__MODULE__{}
|
||||
defstruct [:activity_id, :json, :date, :signature, :digest, :inbox, :unreachable_since]
|
||||
end
|
||||
|
|
@ -22,7 +22,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
|
|||
def follow(target_instance) do
|
||||
with %User{} = local_user <- get_actor(),
|
||||
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
|
||||
{:ok, _, _, activity} <- CommonAPI.follow(local_user, target_user) do
|
||||
{:ok, _, _, activity} <- CommonAPI.follow(target_user, local_user) do
|
||||
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -223,10 +223,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
if Pleroma.Web.Federator.allowed_thread_distance?(reply_depth) and
|
||||
object.data["replies"] != nil do
|
||||
for reply_id <- object.data["replies"] do
|
||||
Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
|
||||
Pleroma.Workers.RemoteFetcherWorker.new(%{
|
||||
"op" => "fetch_remote",
|
||||
"id" => reply_id,
|
||||
"depth" => reply_depth
|
||||
})
|
||||
|> Oban.insert()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -410,10 +412,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
{:ok, expires_at} =
|
||||
Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast(meta[:expires_at])
|
||||
|
||||
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
|
||||
activity_id: meta[:activity_id],
|
||||
expires_at: expires_at
|
||||
})
|
||||
Pleroma.Workers.PurgeExpiredActivity.enqueue(
|
||||
%{
|
||||
activity_id: meta[:activity_id]
|
||||
},
|
||||
scheduled_at: expires_at
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, object, meta}
|
||||
|
|
@ -453,7 +457,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
) do
|
||||
orig_object_ap_id = updated_object["id"]
|
||||
orig_object = Object.get_by_ap_id(orig_object_ap_id)
|
||||
orig_object_data = orig_object.data
|
||||
orig_object_data = Map.get(orig_object, :data)
|
||||
|
||||
updated_object =
|
||||
if meta[:local] do
|
||||
|
|
@ -592,9 +596,9 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
with {:ok, _} <- Repo.delete(object), do: :ok
|
||||
end
|
||||
|
||||
defp send_notifications(meta) do
|
||||
defp stream_notifications(meta) do
|
||||
Keyword.get(meta, :notifications, [])
|
||||
|> Notification.send()
|
||||
|> Notification.stream()
|
||||
|
||||
meta
|
||||
end
|
||||
|
|
@ -625,7 +629,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
@impl true
|
||||
def handle_after_transaction(meta) do
|
||||
meta
|
||||
|> send_notifications()
|
||||
|> stream_notifications()
|
||||
|> send_streamables()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
|
@ -167,7 +168,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def fix_quote_url_and_maybe_fetch(object, options \\ []) do
|
||||
quote_url =
|
||||
case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do
|
||||
case CommonFixes.fix_quote_url(object) do
|
||||
%{"quoteUrl" => quote_url} -> quote_url
|
||||
_ -> nil
|
||||
end
|
||||
|
|
@ -534,6 +535,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
else
|
||||
_ -> e
|
||||
end
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -717,6 +721,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> set_reply_to_uri
|
||||
|> set_quote_url
|
||||
|> set_replies
|
||||
|> CommonFixes.maybe_add_content_map()
|
||||
|> strip_internal_fields
|
||||
|> strip_internal_tags
|
||||
|> set_type
|
||||
|
|
@ -912,9 +917,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def add_emoji_tags(object), do: object
|
||||
|
||||
defp build_emoji_tag({name, url}) do
|
||||
def build_emoji_tag({name, url}) do
|
||||
url = URI.encode(url)
|
||||
|
||||
%{
|
||||
"icon" => %{"url" => "#{URI.encode(url)}", "type" => "Image"},
|
||||
"icon" => %{"url" => "#{url}", "type" => "Image"},
|
||||
"name" => ":" <> name <> ":",
|
||||
"type" => "Emoji",
|
||||
"updated" => "1970-01-01T00:00:00Z",
|
||||
|
|
|
|||
|
|
@ -946,26 +946,14 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec maybe_handle_group_posts(Activity.t()) :: :ok
|
||||
@doc "Automatically repeats posts for local group actor recipients"
|
||||
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
|
||||
User.get_recipients_from_activity(activity)
|
||||
|> Enum.filter(&match?("Group", &1.actor_type))
|
||||
|> Enum.reject(&User.blocks?(&1, poster))
|
||||
|> Enum.each(&Pleroma.Web.CommonAPI.repeat(activity.id, &1))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -127,10 +127,25 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"capabilities" => capabilities,
|
||||
"alsoKnownAs" => user.also_known_as,
|
||||
"vcard:bday" => birthday,
|
||||
"webfinger" => "acct:#{User.full_nickname(user)}"
|
||||
"webfinger" => "acct:#{User.full_nickname(user)}",
|
||||
"published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at)
|
||||
}
|
||||
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|
||||
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|
||||
|> Map.merge(
|
||||
maybe_make_image(
|
||||
&User.avatar_url/2,
|
||||
User.image_description(user.avatar, nil),
|
||||
"icon",
|
||||
user
|
||||
)
|
||||
)
|
||||
|> Map.merge(
|
||||
maybe_make_image(
|
||||
&User.banner_url/2,
|
||||
User.image_description(user.banner, nil),
|
||||
"image",
|
||||
user
|
||||
)
|
||||
)
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
|
@ -305,16 +320,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
end
|
||||
end
|
||||
|
||||
defp maybe_make_image(func, key, user) do
|
||||
defp maybe_make_image(func, description, key, user) do
|
||||
if image = func.(user, no_default: true) do
|
||||
%{
|
||||
key => %{
|
||||
"type" => "Image",
|
||||
"url" => image
|
||||
}
|
||||
key =>
|
||||
%{
|
||||
"type" => "Image",
|
||||
"url" => image
|
||||
}
|
||||
|> maybe_put_description(description)
|
||||
}
|
||||
else
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_put_description(map, description) when is_binary(description) do
|
||||
Map.put(map, "name", description)
|
||||
end
|
||||
|
||||
defp maybe_put_description(map, _description), do: map
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.Stats
|
||||
alias Pleroma.User
|
||||
alias Pleroma.User.Backup
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.AdminAPI
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
|
|
@ -429,7 +430,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|
||||
def create_backup(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_by_nickname(nickname),
|
||||
{:ok, _} <- Pleroma.User.Backup.create(user, admin.id) do
|
||||
%Backup{} = backup <- Backup.new(user),
|
||||
{:ok, inserted_backup} <- Pleroma.Repo.insert(backup),
|
||||
{:ok, %Oban.Job{}} <- Backup.schedule_backup(inserted_backup) do
|
||||
ModerationLog.insert_log(%{actor: admin, subject: user, action: "create_backup"})
|
||||
|
||||
json(conn, "")
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
|
|||
render(conn, "show.json", invite: updated_invite)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ defmodule Pleroma.Web.AdminAPI.RuleController do
|
|||
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :index)
|
||||
|
||||
action_fallback(AdminAPI.FallbackController)
|
||||
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RuleOperation
|
||||
|
||||
|
|
|
|||
|
|
@ -139,7 +139,8 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"Search",
|
||||
"Status actions",
|
||||
"Media attachments",
|
||||
"Bookmark folders"
|
||||
"Bookmark folders",
|
||||
"Tags"
|
||||
]
|
||||
},
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
|||
alias OpenApiSpex.Plug.PutApiSpec
|
||||
alias Plug.Conn
|
||||
|
||||
require Logger
|
||||
|
||||
@impl Plug
|
||||
def init(opts) do
|
||||
opts
|
||||
|
|
@ -51,6 +53,10 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
|||
conn
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error(
|
||||
"Strict ApiSpec: request denied to #{conn.request_path} with params #{inspect(conn.params)}"
|
||||
)
|
||||
|
||||
opts = render_error.init(reason)
|
||||
|
||||
conn
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -497,18 +498,44 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def identity_proofs_operation do
|
||||
def familiar_followers_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve account information"],
|
||||
summary: "Identity proofs",
|
||||
operationId: "AccountController.identity_proofs",
|
||||
# Validators complains about unused path params otherwise
|
||||
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: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
|
||||
Operation.parameter(
|
||||
:id,
|
||||
:query,
|
||||
%Schema{
|
||||
oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}]
|
||||
},
|
||||
"Account IDs",
|
||||
example: "123"
|
||||
)
|
||||
],
|
||||
description: "Not implemented",
|
||||
responses: %{
|
||||
200 => empty_array_response()
|
||||
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
|
||||
|
|
@ -786,6 +813,16 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "User's birthday will be visible"
|
||||
},
|
||||
avatar_description: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Avatar image description."
|
||||
},
|
||||
header_description: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Header image description."
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do
|
|||
security: [%{"oAuth" => ["write:media"]}],
|
||||
requestBody: Helpers.request_body("Parameters", create_request()),
|
||||
responses: %{
|
||||
202 => Operation.response("Media", "application/json", Attachment),
|
||||
200 => Operation.response("Media", "application/json", Attachment),
|
||||
400 => Operation.response("Media", "application/json", ApiError),
|
||||
422 => Operation.response("Media", "application/json", ApiError),
|
||||
500 => Operation.response("Media", "application/json", ApiError)
|
||||
|
|
|
|||
|
|
@ -158,6 +158,10 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
|||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
group_key: %Schema{
|
||||
type: :string,
|
||||
description: "Group key shared by similar notifications"
|
||||
},
|
||||
type: notification_type(),
|
||||
created_at: %Schema{type: :string, format: :"date-time"},
|
||||
account: %Schema{
|
||||
|
|
@ -180,6 +184,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
|||
},
|
||||
example: %{
|
||||
"id" => "34975861",
|
||||
"group-key" => "ungrouped-34975861",
|
||||
"type" => "mention",
|
||||
"created_at" => "2019-11-23T07:49:02.064Z",
|
||||
"account" => Account.schema().example,
|
||||
|
|
@ -202,7 +207,11 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
|||
"pleroma:report",
|
||||
"move",
|
||||
"follow_request",
|
||||
"poll"
|
||||
"poll",
|
||||
"status",
|
||||
"update",
|
||||
"admin.sign_up",
|
||||
"admin.report"
|
||||
],
|
||||
description: """
|
||||
The type of event that resulted in the notification.
|
||||
|
|
@ -216,6 +225,10 @@ 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
|
||||
- `update` - A status you boosted has been edited
|
||||
- `admin.sign_up` - Someone signed up (optionally sent to admins)
|
||||
- `admin.report` - A new report has been filed
|
||||
"""
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -85,9 +85,11 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
|
|||
|
||||
def subscribe_operation do
|
||||
%Operation{
|
||||
deprecated: true,
|
||||
tags: ["Account actions"],
|
||||
summary: "Subscribe",
|
||||
description: "Receive notifications for all statuses posted by the account.",
|
||||
description:
|
||||
"Receive notifications for all statuses posted by the account. Deprecated, use `notify: true` in follow operation instead.",
|
||||
operationId: "PleromaAPI.AccountController.subscribe",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
|
|
@ -100,9 +102,11 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
|
|||
|
||||
def unsubscribe_operation do
|
||||
%Operation{
|
||||
deprecated: true,
|
||||
tags: ["Account actions"],
|
||||
summary: "Unsubscribe",
|
||||
description: "Stop receiving notifications for all statuses posted by the account.",
|
||||
description:
|
||||
"Stop receiving notifications for all statuses posted by the account. Deprecated, use `notify: false` in follow operation instead.",
|
||||
operationId: "PleromaAPI.AccountController.unsubscribe",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
|
|
|
|||
|
|
@ -65,12 +65,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
|
|||
file_name: %Schema{type: :string},
|
||||
file_size: %Schema{type: :integer},
|
||||
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"}
|
||||
tempdir: %Schema{type: :string}
|
||||
},
|
||||
example: %{
|
||||
"content_type" => "application/zip",
|
||||
|
|
@ -79,8 +74,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
|
|||
"file_size" => 4105,
|
||||
"inserted_at" => "2020-09-08T16:42:07.000Z",
|
||||
"processed" => true,
|
||||
"state" => "complete",
|
||||
"processed_number" => 20
|
||||
"tempdir" => "/tmp/PZIMw40vmpM"
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,9 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
|
|||
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
|
||||
"Search type"
|
||||
),
|
||||
Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true),
|
||||
Operation.parameter(:q, :query, %Schema{type: :string}, "The search query",
|
||||
required: true
|
||||
),
|
||||
Operation.parameter(
|
||||
:resolve,
|
||||
:query,
|
||||
|
|
|
|||
|
|
@ -31,11 +31,17 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:ids,
|
||||
:id,
|
||||
:query,
|
||||
%Schema{type: :array, items: FlakeID},
|
||||
"Array of status IDs"
|
||||
),
|
||||
Operation.parameter(
|
||||
:ids,
|
||||
:query,
|
||||
%Schema{type: :array, items: FlakeID},
|
||||
"Deprecated, use `id` instead"
|
||||
),
|
||||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ defmodule Pleroma.Web.ApiSpec.StreamingOperation do
|
|||
end
|
||||
|
||||
defp get_schema(%Schema{} = schema), do: schema
|
||||
defp get_schema(schema), do: schema.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)
|
||||
|
|
|
|||
103
lib/pleroma/web/api_spec/operations/tag_operation.ex
Normal file
103
lib/pleroma/web/api_spec/operations/tag_operation.ex
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
defmodule Pleroma.Web.ApiSpec.TagOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Tag
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Tags"],
|
||||
summary: "Hashtag",
|
||||
description: "View a hashtag",
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "TagController.show",
|
||||
responses: %{
|
||||
200 => Operation.response("Hashtag", "application/json", Tag),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def follow_operation do
|
||||
%Operation{
|
||||
tags: ["Tags"],
|
||||
summary: "Follow a hashtag",
|
||||
description: "Follow a hashtag",
|
||||
security: [%{"oAuth" => ["write:follows"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "TagController.follow",
|
||||
responses: %{
|
||||
200 => Operation.response("Hashtag", "application/json", Tag),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def unfollow_operation do
|
||||
%Operation{
|
||||
tags: ["Tags"],
|
||||
summary: "Unfollow a hashtag",
|
||||
description: "Unfollow a hashtag",
|
||||
security: [%{"oAuth" => ["write:follows"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "TagController.unfollow",
|
||||
responses: %{
|
||||
200 => Operation.response("Hashtag", "application/json", Tag),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_followed_operation do
|
||||
%Operation{
|
||||
tags: ["Tags"],
|
||||
summary: "Followed hashtags",
|
||||
description: "View a list of hashtags the currently authenticated user is following",
|
||||
parameters: pagination_params(),
|
||||
security: [%{"oAuth" => ["read:follows"]}],
|
||||
operationId: "TagController.show_followed",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Hashtags", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: Tag
|
||||
}),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(
|
||||
:id,
|
||||
:path,
|
||||
%Schema{type: :string},
|
||||
"Name of the hashtag"
|
||||
)
|
||||
end
|
||||
|
||||
def pagination_params do
|
||||
[
|
||||
Operation.parameter(:max_id, :query, :integer, "Return items older than this ID"),
|
||||
Operation.parameter(
|
||||
:min_id,
|
||||
:query,
|
||||
:integer,
|
||||
"Return the oldest items newer than this ID"
|
||||
),
|
||||
Operation.parameter(
|
||||
:limit,
|
||||
:query,
|
||||
%Schema{type: :integer, default: 20},
|
||||
"Maximum number of items to return. Will be ignored if it's more than 40"
|
||||
)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
@ -111,7 +111,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
format: :uri,
|
||||
nullable: true,
|
||||
description: "Favicon image of the user's instance"
|
||||
}
|
||||
},
|
||||
avatar_description: %Schema{type: :string},
|
||||
header_description: %Schema{type: :string}
|
||||
}
|
||||
},
|
||||
source: %Schema{
|
||||
|
|
@ -152,6 +154,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
example: %{
|
||||
"acct" => "foobar",
|
||||
"avatar" => "https://mypleroma.com/images/avi.png",
|
||||
"avatar_description" => "",
|
||||
"avatar_static" => "https://mypleroma.com/images/avi.png",
|
||||
"bot" => false,
|
||||
"created_at" => "2020-03-24T13:05:58.000Z",
|
||||
|
|
@ -162,6 +165,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
"followers_count" => 0,
|
||||
"following_count" => 1,
|
||||
"header" => "https://mypleroma.com/images/banner.png",
|
||||
"header_description" => "",
|
||||
"header_static" => "https://mypleroma.com/images/banner.png",
|
||||
"id" => "9tKi3esbG7OQgZ2920",
|
||||
"locked" => false,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
|
|||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
mime_type: %Schema{type: :string, description: "mime type of the attachment"}
|
||||
mime_type: %Schema{type: :string, description: "mime type of the attachment"},
|
||||
name: %Schema{
|
||||
type: :string,
|
||||
description: "Name of the attachment, typically the filename"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
|
|||
},
|
||||
"id" => "1",
|
||||
"unread" => 2,
|
||||
"last_message" => ChatMessage.schema().example(),
|
||||
"last_message" => ChatMessage.schema().example,
|
||||
"updated_at" => "2020-04-21T15:06:45.000Z"
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -249,6 +249,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
nullable: true,
|
||||
description:
|
||||
"A datetime (ISO 8601) that states when the post was pinned or `null` if the post is not pinned"
|
||||
},
|
||||
list_id: %Schema{
|
||||
type: :integer,
|
||||
nullable: true,
|
||||
description:
|
||||
"The ID of the list the post is addressed to (if any, only returned to author)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,11 +17,22 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
|
|||
type: :string,
|
||||
format: :uri,
|
||||
description: "A link to the hashtag on the instance"
|
||||
},
|
||||
following: %Schema{
|
||||
type: :boolean,
|
||||
description: "Whether the authenticated user is following the hashtag"
|
||||
},
|
||||
history: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description:
|
||||
"A list of historical uses of the hashtag (not implemented, for compatibility only)"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
name: "cofe",
|
||||
url: "https://lain.com/tag/cofe"
|
||||
url: "https://lain.com/tag/cofe",
|
||||
following: false
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,4 +10,9 @@ defmodule Pleroma.Web.Auth.Authenticator do
|
|||
@callback handle_error(Plug.Conn.t(), any()) :: any()
|
||||
@callback auth_template() :: String.t() | nil
|
||||
@callback oauth_consumer_template() :: String.t() | nil
|
||||
|
||||
@callback change_password(Pleroma.User.t(), String.t(), String.t(), String.t()) ::
|
||||
{:ok, Pleroma.User.t()} | {:error, term()}
|
||||
|
||||
@optional_callbacks change_password: 4
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,18 +3,14 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Auth.LDAPAuthenticator do
|
||||
alias Pleroma.LDAP
|
||||
alias Pleroma.User
|
||||
|
||||
require Logger
|
||||
|
||||
import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1]
|
||||
import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1]
|
||||
|
||||
@behaviour Pleroma.Web.Auth.Authenticator
|
||||
@base Pleroma.Web.Auth.PleromaAuthenticator
|
||||
|
||||
@connection_timeout 10_000
|
||||
@search_timeout 10_000
|
||||
|
||||
defdelegate get_registration(conn), to: @base
|
||||
defdelegate create_from_registration(conn, registration), to: @base
|
||||
defdelegate handle_error(conn, error), to: @base
|
||||
|
|
@ -24,7 +20,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
|
|||
def get_user(%Plug.Conn{} = conn) do
|
||||
with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])},
|
||||
{:ok, {name, password}} <- fetch_credentials(conn),
|
||||
%User{} = user <- ldap_user(name, password) do
|
||||
%User{} = user <- LDAP.bind_user(name, password) do
|
||||
{:ok, user}
|
||||
else
|
||||
{:ldap, _} ->
|
||||
|
|
@ -35,95 +31,12 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
|
|||
end
|
||||
end
|
||||
|
||||
defp ldap_user(name, password) do
|
||||
ldap = Pleroma.Config.get(:ldap, [])
|
||||
host = Keyword.get(ldap, :host, "localhost")
|
||||
port = Keyword.get(ldap, :port, 389)
|
||||
ssl = Keyword.get(ldap, :ssl, false)
|
||||
sslopts = Keyword.get(ldap, :sslopts, [])
|
||||
|
||||
options =
|
||||
[{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++
|
||||
if sslopts != [], do: [{:sslopts, sslopts}], else: []
|
||||
|
||||
case :eldap.open([to_charlist(host)], options) do
|
||||
{:ok, connection} ->
|
||||
try do
|
||||
if Keyword.get(ldap, :tls, false) do
|
||||
:application.ensure_all_started(:ssl)
|
||||
|
||||
case :eldap.start_tls(
|
||||
connection,
|
||||
Keyword.get(ldap, :tlsopts, []),
|
||||
@connection_timeout
|
||||
) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
error ->
|
||||
Logger.error("Could not start TLS: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
bind_user(connection, ldap, name, password)
|
||||
after
|
||||
:eldap.close(connection)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("Could not open LDAP connection: #{inspect(error)}")
|
||||
{:error, {:ldap_connection_error, error}}
|
||||
def change_password(user, password, new_password, new_password) do
|
||||
case LDAP.change_password(user.nickname, password, new_password) do
|
||||
:ok -> {:ok, user}
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
defp bind_user(connection, ldap, name, password) do
|
||||
uid = Keyword.get(ldap, :uid, "cn")
|
||||
base = Keyword.get(ldap, :base)
|
||||
|
||||
case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
|
||||
:ok ->
|
||||
case fetch_user(name) do
|
||||
%User{} = user ->
|
||||
user
|
||||
|
||||
_ ->
|
||||
register_user(connection, base, uid, name)
|
||||
end
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp register_user(connection, base, uid, name) do
|
||||
case :eldap.search(connection, [
|
||||
{:base, to_charlist(base)},
|
||||
{:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
|
||||
{:scope, :eldap.wholeSubtree()},
|
||||
{:timeout, @search_timeout}
|
||||
]) do
|
||||
{:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} ->
|
||||
params = %{
|
||||
name: name,
|
||||
nickname: name,
|
||||
password: nil
|
||||
}
|
||||
|
||||
params =
|
||||
case List.keyfind(attributes, 'mail', 0) do
|
||||
{_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail))
|
||||
_ -> params
|
||||
end
|
||||
|
||||
changeset = User.register_changeset_ldap(%User{}, params)
|
||||
|
||||
case User.register(changeset) do
|
||||
{:ok, user} -> user
|
||||
error -> error
|
||||
end
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
def change_password(_, _, _, _), do: {:error, :password_confirmation}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
|
|||
alias Pleroma.Registration
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Plugs.AuthenticationPlug
|
||||
|
||||
import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1]
|
||||
|
|
@ -101,4 +102,23 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
|
|||
def auth_template, do: nil
|
||||
|
||||
def oauth_consumer_template, do: nil
|
||||
|
||||
@doc "Changes Pleroma.User password in the database"
|
||||
def change_password(user, password, new_password, new_password) do
|
||||
case CommonAPI.Utils.confirm_current_password(user, password) do
|
||||
{:ok, user} ->
|
||||
with {:ok, _user} <-
|
||||
User.reset_password(user, %{
|
||||
password: new_password,
|
||||
password_confirmation: new_password
|
||||
}) do
|
||||
{:ok, user}
|
||||
end
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def change_password(_, _, _, _), do: {:error, :password_confirmation}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -39,4 +39,8 @@ defmodule Pleroma.Web.Auth.WrapperAuthenticator do
|
|||
implementation().oauth_consumer_template() ||
|
||||
Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")
|
||||
end
|
||||
|
||||
@impl true
|
||||
def change_password(user, password, new_password, new_password_confirmation),
|
||||
do: implementation().change_password(user, password, new_password, new_password_confirmation)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,19 +19,23 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI.ActivityDraft
|
||||
|
||||
import Ecto.Query, only: [where: 3]
|
||||
import Pleroma.Web.Gettext
|
||||
import Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
require Pleroma.Constants
|
||||
require Logger
|
||||
|
||||
def block(blocker, blocked) do
|
||||
@spec block(User.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
|
||||
def block(blocked, blocker) do
|
||||
with {:ok, block_data, _} <- Builder.block(blocker, blocked),
|
||||
{:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
|
||||
{:ok, block}
|
||||
end
|
||||
end
|
||||
|
||||
@spec post_chat_message(User.t(), User.t(), String.t(), list()) ::
|
||||
{:ok, Activity.t()} | Pipeline.errors()
|
||||
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),
|
||||
|
|
@ -54,7 +58,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
)} do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:common_pipeline, {:reject, _} = e} -> e
|
||||
{:common_pipeline, e} -> e
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
|
@ -95,7 +99,9 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def unblock(blocker, blocked) do
|
||||
@spec unblock(User.t(), User.t()) ::
|
||||
{:ok, Activity.t()} | {:ok, :no_activity} | Pipeline.errors() | {:error, :not_blocking}
|
||||
def unblock(blocked, blocker) do
|
||||
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
|
||||
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
|
||||
{:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
|
||||
|
|
@ -114,7 +120,11 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def follow(follower, followed) do
|
||||
@spec follow(User.t(), User.t()) ::
|
||||
{:ok, User.t(), User.t(), Activity.t() | Object.t()}
|
||||
| {:error, :rejected}
|
||||
| Pipeline.errors()
|
||||
def follow(followed, follower) do
|
||||
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
|
||||
|
||||
with {:ok, follow_data, _} <- Builder.follow(follower, followed),
|
||||
|
|
@ -123,12 +133,13 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
if activity.data["state"] == "reject" do
|
||||
{:error, :rejected}
|
||||
else
|
||||
{:ok, follower, followed, activity}
|
||||
{:ok, followed, follower, activity}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow(follower, unfollowed) do
|
||||
@spec unfollow(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()}
|
||||
def unfollow(unfollowed, follower) do
|
||||
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
|
||||
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
|
||||
{:ok, _subscription} <- User.unsubscribe(follower, unfollowed),
|
||||
|
|
@ -137,6 +148,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors()
|
||||
def accept_follow_request(follower, followed) do
|
||||
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
||||
{:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
|
||||
|
|
@ -145,6 +157,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors() | nil
|
||||
def reject_follow_request(follower, followed) do
|
||||
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
||||
{:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
|
||||
|
|
@ -153,9 +166,12 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec delete(String.t(), User.t()) ::
|
||||
{:ok, Activity.t()} | Pipeline.errors() | {:error, :not_found | String.t()}
|
||||
def delete(activity_id, user) do
|
||||
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
|
||||
{:find_activity, Activity.get_by_id(activity_id, filter: [])},
|
||||
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(activity)},
|
||||
{_, %Object{} = object, _} <-
|
||||
{:find_object, Object.normalize(activity, fetch: false), activity},
|
||||
true <- User.privileged?(user, :messages_delete) || user.ap_id == object.data["actor"],
|
||||
|
|
@ -201,6 +217,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, :not_found}
|
||||
def repeat(id, user, params \\ %{}) do
|
||||
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
|
||||
object = %Object{} <- Object.normalize(activity, fetch: false),
|
||||
|
|
@ -218,11 +235,13 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, :not_found | String.t()}
|
||||
def unrepeat(id, user) do
|
||||
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
||||
{:find_activity, Activity.get_by_id(id)},
|
||||
%Object{} = note <- Object.normalize(activity, fetch: false),
|
||||
%Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
|
||||
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(announce)},
|
||||
{:ok, undo, _} <- Builder.undo(user, announce),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
|
||||
{:ok, activity}
|
||||
|
|
@ -232,8 +251,9 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
|
||||
def favorite(%User{} = user, id) do
|
||||
@spec favorite(String.t(), User.t()) ::
|
||||
{:ok, Activity.t()} | {:ok, :already_liked} | {:error, :not_found | String.t()}
|
||||
def favorite(id, %User{} = user) do
|
||||
case favorite_helper(user, id) do
|
||||
{:ok, _} = res ->
|
||||
res
|
||||
|
|
@ -247,7 +267,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def favorite_helper(user, id) do
|
||||
defp favorite_helper(user, id) do
|
||||
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
|
||||
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
|
||||
{_, {:ok, %Activity{} = activity, _meta}} <-
|
||||
|
|
@ -270,11 +290,14 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec unfavorite(String.t(), User.t()) ::
|
||||
{:ok, Activity.t()} | {:error, :not_found | String.t()}
|
||||
def unfavorite(id, user) do
|
||||
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
||||
{:find_activity, Activity.get_by_id(id)},
|
||||
%Object{} = note <- Object.normalize(activity, fetch: false),
|
||||
%Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
|
||||
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(like)},
|
||||
{:ok, undo, _} <- Builder.undo(user, like),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
|
||||
{:ok, activity}
|
||||
|
|
@ -284,6 +307,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec react_with_emoji(String.t(), User.t(), String.t()) ::
|
||||
{:ok, Activity.t()} | {:error, String.t()}
|
||||
def react_with_emoji(id, user, emoji) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
object <- Object.normalize(activity, fetch: false),
|
||||
|
|
@ -296,8 +321,11 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec unreact_with_emoji(String.t(), User.t(), String.t()) ::
|
||||
{:ok, Activity.t()} | {:error, String.t()}
|
||||
def unreact_with_emoji(id, user, emoji) do
|
||||
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
|
||||
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(reaction_activity)},
|
||||
{:ok, undo, _} <- Builder.undo(user, reaction_activity),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
|
||||
{:ok, activity}
|
||||
|
|
@ -307,7 +335,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
|
||||
@spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | Pipeline.errors()
|
||||
def vote(%Object{data: %{"type" => "Question"}} = object, %User{} = user, choices) do
|
||||
with :ok <- validate_not_author(object, user),
|
||||
:ok <- validate_existing_votes(user, object),
|
||||
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
|
||||
|
|
@ -368,14 +397,16 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def public_announce?(_, %{visibility: visibility})
|
||||
when visibility in ~w{public unlisted private direct},
|
||||
do: visibility in ~w(public unlisted)
|
||||
defp public_announce?(_, %{visibility: visibility})
|
||||
when visibility in ~w{public unlisted private direct},
|
||||
do: visibility in ~w(public unlisted)
|
||||
|
||||
def public_announce?(object, _) do
|
||||
defp public_announce?(object, _) do
|
||||
Visibility.public?(object)
|
||||
end
|
||||
|
||||
@spec get_visibility(map(), map() | nil, Participation.t() | nil) ::
|
||||
{String.t() | nil, String.t() | nil}
|
||||
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
|
||||
|
||||
def get_visibility(%{visibility: visibility}, in_reply_to, _)
|
||||
|
|
@ -394,6 +425,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
|
||||
|
||||
@spec get_replied_to_visibility(Activity.t() | nil) :: String.t() | nil
|
||||
def get_replied_to_visibility(nil), do: nil
|
||||
|
||||
def get_replied_to_visibility(activity) do
|
||||
|
|
@ -402,6 +434,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec check_expiry_date({:ok, nil | integer()} | String.t()) ::
|
||||
{:ok, boolean() | nil} | {:error, String.t()}
|
||||
def check_expiry_date({:ok, nil} = res), do: res
|
||||
|
||||
def check_expiry_date({:ok, in_seconds}) do
|
||||
|
|
@ -419,19 +453,22 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|> check_expiry_date()
|
||||
end
|
||||
|
||||
@spec listen(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def listen(user, data) do
|
||||
with {:ok, draft} <- ActivityDraft.listen(user, data) do
|
||||
ActivityPub.listen(draft.changes)
|
||||
end
|
||||
end
|
||||
|
||||
@spec post(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def post(user, %{status: _} = data) do
|
||||
with {:ok, draft} <- ActivityDraft.create(user, data) do
|
||||
ActivityPub.create(draft.changes, draft.preview?)
|
||||
end
|
||||
end
|
||||
|
||||
def update(user, orig_activity, changes) do
|
||||
@spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, nil}
|
||||
def update(orig_activity, %User{} = user, changes) do
|
||||
with orig_object <- Object.normalize(orig_activity),
|
||||
{:ok, new_object} <- make_update_data(user, orig_object, changes),
|
||||
{:ok, update_data, _} <- Builder.update(user, new_object),
|
||||
|
|
@ -466,7 +503,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
|
||||
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
|
||||
def pin(id, %User{} = user) do
|
||||
with %Activity{} = activity <- create_activity_by_id(id),
|
||||
true <- activity_belongs_to_actor(activity, user.ap_id),
|
||||
|
|
@ -506,7 +543,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
|
||||
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
|
||||
def unpin(id, user) do
|
||||
with %Activity{} = activity <- create_activity_by_id(id),
|
||||
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
|
||||
|
|
@ -521,17 +558,18 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def add_mute(user, activity, params \\ %{}) do
|
||||
@spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, String.t()}
|
||||
def add_mute(activity, user, params \\ %{}) do
|
||||
expires_in = Map.get(params, :expires_in, 0)
|
||||
|
||||
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
|
||||
_ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
|
||||
if expires_in > 0 do
|
||||
Pleroma.Workers.MuteExpireWorker.enqueue(
|
||||
"unmute_conversation",
|
||||
%{"user_id" => user.id, "activity_id" => activity.id},
|
||||
Pleroma.Workers.MuteExpireWorker.new(
|
||||
%{"op" => "unmute_conversation", "user_id" => user.id, "activity_id" => activity.id},
|
||||
schedule_in: expires_in
|
||||
)
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
{:ok, activity}
|
||||
|
|
@ -540,15 +578,17 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def remove_mute(%User{} = user, %Activity{} = activity) do
|
||||
@spec remove_mute(Activity.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def remove_mute(%Activity{} = activity, %User{} = user) do
|
||||
ThreadMute.remove_mute(user.id, activity.data["context"])
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
def remove_mute(user_id, activity_id) do
|
||||
@spec remove_mute(String.t(), String.t()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def remove_mute(activity_id, user_id) do
|
||||
with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
|
||||
{:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
|
||||
remove_mute(user, activity)
|
||||
remove_mute(activity, user)
|
||||
else
|
||||
{what, result} = error ->
|
||||
Logger.warning(
|
||||
|
|
@ -559,13 +599,15 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
|
||||
@spec thread_muted?(Activity.t(), User.t()) :: boolean()
|
||||
def thread_muted?(%{data: %{"context" => context}}, %User{id: user_id})
|
||||
when is_binary(context) do
|
||||
ThreadMute.exists?(user_id, context)
|
||||
end
|
||||
|
||||
def thread_muted?(_, _), do: false
|
||||
|
||||
@spec report(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def report(user, data) do
|
||||
with {:ok, account} <- get_reported_account(data.account_id),
|
||||
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
|
||||
|
|
@ -599,6 +641,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|> Enum.filter(&Rule.exists?/1)
|
||||
end
|
||||
|
||||
@spec update_report_state(String.t() | [String.t()], String.t()) ::
|
||||
{:ok, any()} | {:error, any()}
|
||||
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}
|
||||
|
|
@ -611,17 +655,16 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
Utils.update_report_state(activity, state)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
_ -> {:error, dgettext("errors", "Could not update state")}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_activity_scope(String.t(), map()) :: {:ok, any()} | {:error, any()}
|
||||
def update_activity_scope(activity_id, opts \\ %{}) do
|
||||
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
|
||||
{:ok, activity} <- toggle_sensitive(activity, opts) do
|
||||
set_visibility(activity, opts)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -649,14 +692,17 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
defp set_visibility(activity, _), do: {:ok, activity}
|
||||
|
||||
def hide_reblogs(%User{} = user, %User{} = target) do
|
||||
@spec hide_reblogs(User.t(), User.t()) :: {:ok, any()} | {:error, any()}
|
||||
def hide_reblogs(%User{} = target, %User{} = user) do
|
||||
UserRelationship.create_reblog_mute(user, target)
|
||||
end
|
||||
|
||||
def show_reblogs(%User{} = user, %User{} = target) do
|
||||
@spec show_reblogs(User.t(), User.t()) :: {:ok, any()} | {:error, any()}
|
||||
def show_reblogs(%User{} = target, %User{} = user) do
|
||||
UserRelationship.delete_reblog_mute(user, target)
|
||||
end
|
||||
|
||||
@spec get_user(String.t(), boolean()) :: User.t() | nil
|
||||
def get_user(ap_id, fake_record_fallback \\ true) do
|
||||
cond do
|
||||
user = User.get_cached_by_ap_id(ap_id) ->
|
||||
|
|
@ -673,4 +719,14 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_cancel_jobs(%Activity{id: activity_id}) do
|
||||
Oban.Job
|
||||
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
|
||||
|> where([j], j.args["op"] == "publish_one")
|
||||
|> where([j], j.args["params"]["activity_id"] == ^activity_id)
|
||||
|> Oban.cancel_all_jobs()
|
||||
end
|
||||
|
||||
defp maybe_cancel_jobs(_), do: {:ok, 0}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -135,8 +135,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
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ defmodule Pleroma.Web.Endpoint do
|
|||
websocket: [
|
||||
path: "/",
|
||||
compress: false,
|
||||
connect_info: [:sec_websocket_protocol],
|
||||
error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []},
|
||||
fullsweep_after: 20
|
||||
]
|
||||
|
|
@ -38,6 +39,8 @@ defmodule Pleroma.Web.Endpoint do
|
|||
|
||||
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
|
||||
|
||||
plug(Pleroma.Web.Plugs.LoggerMetadataPath)
|
||||
|
||||
plug(Pleroma.Web.Plugs.SetLocalePlug)
|
||||
plug(CORSPlug)
|
||||
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
|
|||
redirector_with_meta(conn, %{user: user})
|
||||
else
|
||||
nil ->
|
||||
redirector(conn, params)
|
||||
redirector_with_meta(conn, Map.delete(params, "maybe_nickname_or_id"))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -36,19 +36,29 @@ defmodule Pleroma.Web.Federator do
|
|||
|
||||
# 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)},
|
||||
ReceiverWorker.new(
|
||||
%{
|
||||
"op" => "incoming_ap_doc",
|
||||
"req_headers" => req_headers,
|
||||
"params" => params,
|
||||
"timeout" => :timer.seconds(20)
|
||||
},
|
||||
priority: 2
|
||||
)
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
def incoming_ap_doc(%{"type" => "Delete"} = params) do
|
||||
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params}, priority: 3)
|
||||
ReceiverWorker.new(%{"op" => "incoming_ap_doc", "params" => params},
|
||||
priority: 3,
|
||||
queue: :slow
|
||||
)
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
def incoming_ap_doc(params) do
|
||||
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params})
|
||||
ReceiverWorker.new(%{"op" => "incoming_ap_doc", "params" => params})
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -58,9 +68,10 @@ defmodule Pleroma.Web.Federator do
|
|||
|
||||
@impl true
|
||||
def publish(%Pleroma.Activity{data: %{"type" => type}} = activity) do
|
||||
PublisherWorker.enqueue("publish", %{"activity_id" => activity.id},
|
||||
PublisherWorker.new(%{"op" => "publish", "activity_id" => activity.id},
|
||||
priority: publish_priority(type)
|
||||
)
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
defp publish_priority("Delete"), do: 3
|
||||
|
|
@ -69,7 +80,10 @@ defmodule Pleroma.Web.Federator do
|
|||
# Job Worker Callbacks
|
||||
|
||||
@spec perform(atom(), any()) :: {:ok, any()} | {:error, any()}
|
||||
def perform(:publish_one, params), do: Publisher.publish_one(params)
|
||||
def perform(:publish_one, params) do
|
||||
Publisher.prepare_one(params)
|
||||
|> Publisher.publish_one()
|
||||
end
|
||||
|
||||
def perform(:publish, activity) do
|
||||
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
|
||||
|
|
@ -88,7 +102,8 @@ 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, User.get_or_fetch_by_ap_id(actor)},
|
||||
with {_, {:ok, user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)},
|
||||
{:user_active, true} <- {:user_active, match?(true, user.is_active)},
|
||||
nil <- Activity.normalize(params["id"]),
|
||||
{_, :ok} <-
|
||||
{:correct_origin?, Containment.contain_origin_from_id(actor, params)},
|
||||
|
|
@ -107,11 +122,6 @@ defmodule Pleroma.Web.Federator do
|
|||
Logger.debug("Unhandled actor #{actor}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
{:error, {:validate_object, _}} = e ->
|
||||
Logger.error("Incoming AP doc validation error: #{inspect(e)}")
|
||||
Logger.debug(Jason.encode!(params, pretty: true))
|
||||
e
|
||||
|
||||
e ->
|
||||
# Just drop those for now
|
||||
Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ defmodule Pleroma.Web.Feed.TagController do
|
|||
alias Pleroma.Web.Feed.FeedView
|
||||
|
||||
def feed(conn, params) do
|
||||
if Config.get!([:instance, :public]) do
|
||||
if not Config.restrict_unauthenticated_access?(:timelines, :local) do
|
||||
render_feed(conn, params)
|
||||
else
|
||||
render_error(conn, :not_found, "Not found")
|
||||
|
|
@ -18,10 +18,12 @@ defmodule Pleroma.Web.Feed.TagController do
|
|||
end
|
||||
|
||||
defp render_feed(conn, %{"tag" => raw_tag} = params) do
|
||||
local_only = Config.restrict_unauthenticated_access?(:timelines, :federated)
|
||||
|
||||
{format, tag} = parse_tag(raw_tag)
|
||||
|
||||
activities =
|
||||
%{type: ["Create"], tag: tag}
|
||||
%{type: ["Create"], tag: tag, local_only: local_only}
|
||||
|> Pleroma.Maps.put_if_present(:max_id, params["max_id"])
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ defmodule Pleroma.Web.Feed.UserController do
|
|||
|
||||
action_fallback(:errors)
|
||||
|
||||
def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do
|
||||
def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname} = params) do
|
||||
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
|
||||
Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, %{user: user})
|
||||
else
|
||||
_ -> Pleroma.Web.Fallback.RedirectController.redirector(conn, nil)
|
||||
_ -> Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, params)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.ListView
|
||||
alias Pleroma.Web.MastodonAPI.MastodonAPI
|
||||
alias Pleroma.Web.MastodonAPI.MastodonAPIController
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.OAuth.OAuthController
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
|
@ -51,7 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:accounts"]}
|
||||
when action in [:verify_credentials, :endorsements, :identity_proofs]
|
||||
when action in [:verify_credentials, :endorsements]
|
||||
)
|
||||
|
||||
plug(
|
||||
|
|
@ -72,7 +71,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,
|
||||
|
|
@ -230,6 +232,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|> Maps.put_if_present(:is_discoverable, params[:discoverable])
|
||||
|> Maps.put_if_present(:birthday, params[:birthday])
|
||||
|> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
|
||||
|> Maps.put_if_present(:avatar_description, params[:avatar_description])
|
||||
|> Maps.put_if_present(:header_description, params[:header_description])
|
||||
|
||||
# What happens here:
|
||||
#
|
||||
|
|
@ -275,6 +279,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
{:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "Name is too long")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:avatar_description, {_, _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "Avatar description is too long")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:header_description, {_, _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "Banner description is too long")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "One or more field entries are too long")
|
||||
|
||||
|
|
@ -457,7 +467,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
|
||||
with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
|
||||
with {:ok, follower} <- CommonAPI.unfollow(followed, follower) do
|
||||
render(conn, "relationship.json", user: follower, target: followed)
|
||||
end
|
||||
end
|
||||
|
|
@ -492,7 +502,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
@doc "POST /api/v1/accounts/:id/block"
|
||||
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
|
||||
with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
|
||||
with {:ok, _activity} <- CommonAPI.block(blocked, blocker) do
|
||||
render(conn, "relationship.json", user: blocker, target: blocked)
|
||||
else
|
||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||
|
|
@ -501,7 +511,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
@doc "POST /api/v1/accounts/:id/unblock"
|
||||
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
|
||||
with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
|
||||
with {:ok, _activity} <- CommonAPI.unblock(blocked, blocker) do
|
||||
render(conn, "relationship.json", user: blocker, target: blocked)
|
||||
else
|
||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||
|
|
@ -629,6 +639,32 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
)
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/identity_proofs"
|
||||
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
|
||||
@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
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
|
|||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
plug(Pleroma.Web.Plugs.RateLimiter, [name: :oauth_app_creation] when action == :create)
|
||||
|
||||
plug(:skip_auth when action in [:create, :verify_credentials])
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Web.MastodonAPI.MarkerController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
|
@ -30,9 +31,16 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
|
|||
def upsert(%{assigns: %{user: user}, body_params: params} = conn, _) do
|
||||
params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
|
||||
|
||||
with {:ok, result} <- Pleroma.Marker.upsert(user, params),
|
||||
with {:ok, _} <- mark_notifications_read(user, params),
|
||||
{:ok, result} <- Pleroma.Marker.upsert(user, params),
|
||||
markers <- Map.values(result) do
|
||||
render(conn, "markers.json", %{markers: markers})
|
||||
end
|
||||
end
|
||||
|
||||
defp mark_notifications_read(user, %{"notifications" => %{last_read_id: last_read_id}}) do
|
||||
Pleroma.Notification.set_read_up_to(user, last_read_id)
|
||||
end
|
||||
|
||||
defp mark_notifications_read(_, _), do: {:ok, :noop}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -53,9 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
|
|||
) do
|
||||
attachment_data = Map.put(object.data, "id", object.id)
|
||||
|
||||
conn
|
||||
|> put_status(202)
|
||||
|> render("attachment.json", %{attachment: attachment_data})
|
||||
render(conn, "attachment.json", %{attachment: attachment_data})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
|||
pleroma:emoji_reaction
|
||||
poll
|
||||
update
|
||||
status
|
||||
}
|
||||
|
||||
# GET /api/v1/notifications
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
|
|||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Workers.PollWorker
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
|
|
@ -27,12 +28,16 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
|
|||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
@poll_refresh_interval 120
|
||||
|
||||
@doc "GET /api/v1/polls/:id"
|
||||
def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
|
||||
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
|
||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
||||
with %Object{} = object <- Object.get_by_id(id),
|
||||
%Activity{} = activity <-
|
||||
Activity.get_create_by_object_ap_id_with_object(object.data["id"]),
|
||||
true <- Visibility.visible_for_user?(activity, user) do
|
||||
maybe_refresh_poll(activity)
|
||||
|
||||
try_render(conn, "show.json", %{object: object, for: user})
|
||||
else
|
||||
error when is_nil(error) or error == false ->
|
||||
|
|
@ -51,7 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
|
|||
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
|
||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
||||
true <- Visibility.visible_for_user?(activity, user),
|
||||
{:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
|
||||
{:ok, _activities, object} <- get_cached_vote_or_vote(object, user, choices) do
|
||||
try_render(conn, "show.json", %{object: object, for: user})
|
||||
else
|
||||
nil -> render_error(conn, :not_found, "Record not found")
|
||||
|
|
@ -60,14 +65,23 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
|
|||
end
|
||||
end
|
||||
|
||||
defp get_cached_vote_or_vote(user, object, choices) do
|
||||
defp get_cached_vote_or_vote(object, user, choices) do
|
||||
idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
|
||||
|
||||
@cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
|
||||
case CommonAPI.vote(user, object, choices) do
|
||||
case CommonAPI.vote(object, user, choices) do
|
||||
{:error, _message} = res -> {:ignore, res}
|
||||
res -> {:commit, res}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_refresh_poll(%Activity{object: %Object{} = object} = activity) do
|
||||
with false <- activity.local,
|
||||
{:ok, end_time} <- NaiveDateTime.from_iso8601(object.data["closed"]),
|
||||
{_, :lt} <- {:closed_compare, NaiveDateTime.compare(object.updated_at, end_time)} do
|
||||
PollWorker.new(%{"op" => "refresh", "activity_id" => activity.id})
|
||||
|> Oban.insert(unique: [period: @poll_refresh_interval])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -111,10 +111,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
`ids` query param is required
|
||||
"""
|
||||
def index(
|
||||
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{ids: ids} = params}}} =
|
||||
%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} =
|
||||
conn,
|
||||
_
|
||||
) do
|
||||
ids = Map.get(params, :id, Map.get(params, :ids))
|
||||
limit = 100
|
||||
|
||||
activities =
|
||||
|
|
@ -276,7 +277,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
actor <- Activity.user_actor(activity),
|
||||
{_, true} <- {:own_status, actor.id == user.id},
|
||||
changes <- body_params |> put_application(conn),
|
||||
{_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
|
||||
{_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(activity, user, changes)},
|
||||
{_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
|
||||
try_render(conn, "show.json",
|
||||
activity: activity,
|
||||
|
|
@ -357,7 +358,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
conn,
|
||||
_
|
||||
) do
|
||||
with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
|
||||
with {:ok, _fav} <- CommonAPI.favorite(activity_id, user),
|
||||
%Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
|
||||
end
|
||||
|
|
@ -453,7 +454,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
_
|
||||
) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
{:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
|
||||
{:ok, activity} <- CommonAPI.add_mute(activity, user, params) do
|
||||
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
|
||||
end
|
||||
end
|
||||
|
|
@ -467,7 +468,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
_
|
||||
) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
{:ok, activity} <- CommonAPI.remove_mute(user, activity) do
|
||||
{:ok, activity} <- CommonAPI.remove_mute(activity, user) do
|
||||
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
77
lib/pleroma/web/mastodon_api/controllers/tag_controller.ex
Normal file
77
lib/pleroma/web/mastodon_api/controllers/tag_controller.ex
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.TagController do
|
||||
@moduledoc "Hashtag routes for mastodon API"
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Hashtag
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.User
|
||||
|
||||
import Pleroma.Web.ControllerHelper,
|
||||
only: [
|
||||
add_link_headers: 2
|
||||
]
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(
|
||||
Pleroma.Web.Plugs.OAuthScopesPlug,
|
||||
%{scopes: ["read"]} when action in [:show]
|
||||
)
|
||||
|
||||
plug(
|
||||
Pleroma.Web.Plugs.OAuthScopesPlug,
|
||||
%{scopes: ["read:follows"]} when action in [:show_followed]
|
||||
)
|
||||
|
||||
plug(
|
||||
Pleroma.Web.Plugs.OAuthScopesPlug,
|
||||
%{scopes: ["write:follows"]} when action in [:follow, :unfollow]
|
||||
)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TagOperation
|
||||
|
||||
def show(conn, %{id: id}) do
|
||||
with %Hashtag{} = hashtag <- Hashtag.get_by_name(id) do
|
||||
render(conn, "show.json", tag: hashtag, for_user: conn.assigns.user)
|
||||
else
|
||||
_ -> conn |> render_error(:not_found, "Hashtag not found")
|
||||
end
|
||||
end
|
||||
|
||||
def follow(conn, %{id: id}) do
|
||||
with %Hashtag{} = hashtag <- Hashtag.get_by_name(id),
|
||||
%User{} = user <- conn.assigns.user,
|
||||
{:ok, _} <-
|
||||
User.follow_hashtag(user, hashtag) do
|
||||
render(conn, "show.json", tag: hashtag, for_user: user)
|
||||
else
|
||||
_ -> render_error(conn, :not_found, "Hashtag not found")
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow(conn, %{id: id}) do
|
||||
with %Hashtag{} = hashtag <- Hashtag.get_by_name(id),
|
||||
%User{} = user <- conn.assigns.user,
|
||||
{:ok, _} <-
|
||||
User.unfollow_hashtag(user, hashtag) do
|
||||
render(conn, "show.json", tag: hashtag, for_user: user)
|
||||
else
|
||||
_ -> render_error(conn, :not_found, "Hashtag not found")
|
||||
end
|
||||
end
|
||||
|
||||
def show_followed(conn, params) do
|
||||
with %{assigns: %{user: %User{} = user}} <- conn do
|
||||
params = Map.put(params, :id_type, :integer)
|
||||
|
||||
hashtags =
|
||||
user
|
||||
|> User.HashtagFollow.followed_hashtags_query()
|
||||
|> Pagination.fetch_paginated(params)
|
||||
|
||||
conn
|
||||
|> add_link_headers(hashtags)
|
||||
|> render("index.json", tags: hashtags, for_user: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -40,6 +40,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
|
||||
# GET /api/v1/timelines/home
|
||||
def home(%{assigns: %{user: user}} = conn, params) do
|
||||
followed_hashtags =
|
||||
user
|
||||
|> User.followed_hashtags()
|
||||
|> Enum.map(& &1.id)
|
||||
|
||||
params =
|
||||
params
|
||||
|> Map.put(:type, ["Create", "Announce"])
|
||||
|
|
@ -49,6 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
|> Map.put(:announce_filtering_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|> Map.put(:local_only, params[:local])
|
||||
|> Map.put(:followed_hashtags, followed_hashtags)
|
||||
|> Map.delete(:local)
|
||||
|
||||
activities =
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|
|||
def follow(follower, followed, params \\ %{}) do
|
||||
result =
|
||||
if not User.following?(follower, followed) do
|
||||
CommonAPI.follow(follower, followed)
|
||||
CommonAPI.follow(followed, follower)
|
||||
else
|
||||
{:ok, follower, followed, nil}
|
||||
{:ok, followed, follower, nil}
|
||||
end
|
||||
|
||||
with {:ok, follower, _followed, _} <- result do
|
||||
with {:ok, _followed, follower, _} <- result do
|
||||
options = cast_params(params)
|
||||
set_reblogs_visibility(options[:reblogs], result)
|
||||
set_subscription(options[:notify], result)
|
||||
|
|
@ -29,19 +29,19 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
defp set_reblogs_visibility(false, {:ok, follower, followed, _}) do
|
||||
CommonAPI.hide_reblogs(follower, followed)
|
||||
defp set_reblogs_visibility(false, {:ok, followed, follower, _}) do
|
||||
CommonAPI.hide_reblogs(followed, follower)
|
||||
end
|
||||
|
||||
defp set_reblogs_visibility(_, {:ok, follower, followed, _}) do
|
||||
CommonAPI.show_reblogs(follower, followed)
|
||||
defp set_reblogs_visibility(_, {:ok, followed, follower, _}) do
|
||||
CommonAPI.show_reblogs(followed, follower)
|
||||
end
|
||||
|
||||
defp set_subscription(true, {:ok, follower, followed, _}) do
|
||||
defp set_subscription(true, {:ok, followed, follower, _}) do
|
||||
User.subscribe(follower, followed)
|
||||
end
|
||||
|
||||
defp set_subscription(false, {:ok, follower, followed, _}) do
|
||||
defp set_subscription(false, {:ok, followed, follower, _}) do
|
||||
User.unsubscribe(follower, followed)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -92,14 +92,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
User.get_follow_state(reading_user, target)
|
||||
end
|
||||
|
||||
followed_by =
|
||||
if following_relationships do
|
||||
case FollowingRelationship.find(following_relationships, target, reading_user) do
|
||||
%{state: :follow_accept} -> true
|
||||
_ -> false
|
||||
end
|
||||
else
|
||||
User.following?(target, reading_user)
|
||||
followed_by = FollowingRelationship.following?(target, reading_user)
|
||||
following = FollowingRelationship.following?(reading_user, target)
|
||||
|
||||
requested =
|
||||
cond do
|
||||
following -> false
|
||||
true -> match?(:follow_pending, follow_state)
|
||||
end
|
||||
|
||||
subscribing =
|
||||
|
|
@ -114,7 +113,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
# NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
|
||||
%{
|
||||
id: to_string(target.id),
|
||||
following: follow_state == :follow_accept,
|
||||
following: following,
|
||||
followed_by: followed_by,
|
||||
blocking:
|
||||
UserRelationship.exists?(
|
||||
|
|
@ -150,7 +149,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
),
|
||||
subscribing: subscribing,
|
||||
notifying: subscribing,
|
||||
requested: follow_state == :follow_pending,
|
||||
requested: requested,
|
||||
domain_blocking: User.blocks_domain?(reading_user, target),
|
||||
showing_reblogs:
|
||||
not UserRelationship.exists?(
|
||||
|
|
@ -193,6 +192,25 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
render_many(targets, AccountView, "relationship.json", render_opts)
|
||||
end
|
||||
|
||||
def render("familiar_followers.json", %{users: users} = opts) do
|
||||
opts =
|
||||
opts
|
||||
|> Map.merge(%{as: :user})
|
||||
|> Map.delete(:users)
|
||||
|
||||
users
|
||||
|> render_many(AccountView, "familiar_followers.json", opts)
|
||||
end
|
||||
|
||||
def render("familiar_followers.json", %{user: %{id: id, accounts: accounts}} = opts) do
|
||||
accounts =
|
||||
accounts
|
||||
|> render_many(AccountView, "show.json", opts)
|
||||
|> Enum.filter(&Enum.any?/1)
|
||||
|
||||
%{id: id, accounts: accounts}
|
||||
end
|
||||
|
||||
defp do_render("show.json", %{user: user} = opts) do
|
||||
self = opts[:for] == user
|
||||
|
||||
|
|
@ -201,8 +219,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
|
||||
avatar = User.avatar_url(user) |> MediaProxy.url()
|
||||
avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true)
|
||||
avatar_description = User.image_description(user.avatar)
|
||||
header = User.banner_url(user) |> MediaProxy.url()
|
||||
header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
|
||||
header_description = User.image_description(user.banner)
|
||||
|
||||
following_count =
|
||||
if !user.hide_follows_count or !user.hide_follows or self,
|
||||
|
|
@ -303,7 +323,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
skip_thread_containment: user.skip_thread_containment,
|
||||
background_image: image_url(user.background) |> MediaProxy.url(),
|
||||
accepts_chat_messages: user.accepts_chat_messages,
|
||||
favicon: favicon
|
||||
favicon: favicon,
|
||||
avatar_description: avatar_description,
|
||||
header_description: header_description
|
||||
}
|
||||
}
|
||||
|> maybe_put_role(user, opts[:for])
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
|
||||
def federation do
|
||||
quarantined = Config.get([:instance, :quarantined_instances], [])
|
||||
rejected = Config.get([:instance, :rejected_instances], [])
|
||||
|
||||
if Config.get([:mrf, :transparency]) do
|
||||
{:ok, data} = MRF.describe()
|
||||
|
|
@ -174,6 +175,12 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
|> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
|
||||
|> Map.new()
|
||||
})
|
||||
|> Map.put(
|
||||
:rejected_instances,
|
||||
rejected
|
||||
|> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
|
||||
|> Map.new()
|
||||
)
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
|
||||
response = %{
|
||||
id: to_string(notification.id),
|
||||
group_key: "ungrouped-" <> to_string(notification.id),
|
||||
type: notification.type,
|
||||
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
|
||||
account: account,
|
||||
|
|
@ -108,6 +109,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
"mention" ->
|
||||
put_status(response, activity, reading_user, status_render_opts)
|
||||
|
||||
"status" ->
|
||||
put_status(response, activity, reading_user, status_render_opts)
|
||||
|
||||
"favourite" ->
|
||||
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
|
||||
|
||||
|
|
|
|||
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