Merge pull request 'Allow fine-grained announce visibilities (ported from Akkoma)' (#7832) from mkljczk/pleroma:boost-visibilities into develop

Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/7832
This commit is contained in:
nicole mikołajczyk 2026-03-21 20:45:30 +00:00
commit 6bbfba7f6e
6 changed files with 92 additions and 62 deletions

View file

@ -0,0 +1 @@
Allow fine-grained announce visibilities

View file

@ -332,21 +332,18 @@ defmodule Pleroma.Web.ActivityPub.Builder do
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()} @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
def announce(actor, object, options \\ []) do def announce(actor, object, options \\ []) do
public? = Keyword.get(options, :public, false) visibility = Keyword.get(options, :visibility, "public")
to = {to, cc} =
cond do if actor.ap_id == Relay.ap_id() do
actor.ap_id == Relay.ap_id() -> {[actor.follower_address], []}
[actor.follower_address] else
Pleroma.Web.CommonAPI.Utils.get_to_and_cc_for_visibility(
public? and Visibility.local_public?(object) -> visibility,
[actor.follower_address, object.data["actor"], Utils.as_local_public()] actor.follower_address,
nil,
public? -> [object.data["actor"]]
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()] )
true ->
[actor.follower_address, object.data["actor"]]
end end
{:ok, {:ok,
@ -355,6 +352,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
"actor" => actor.ap_id, "actor" => actor.ap_id,
"object" => object.data["id"], "object" => object.data["id"],
"to" => to, "to" => to,
"cc" => cc,
"context" => object.data["context"], "context" => object.data["context"],
"type" => "Announce", "type" => "Announce",
"published" => Utils.make_date() "published" => Utils.make_date()

View file

@ -222,8 +222,8 @@ defmodule Pleroma.Web.CommonAPI do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
object = %Object{} <- Object.normalize(activity, fetch: false), object = %Object{} <- Object.normalize(activity, fetch: false),
{_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)}, {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
public = public_announce?(object, params), visibility = announce_visibility(object, params),
{:ok, announce, _} <- Builder.announce(user, object, public: public), {:ok, announce, _} <- Builder.announce(user, object, visibility: visibility),
{:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
{:ok, activity} {:ok, activity}
else else
@ -407,13 +407,11 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
defp public_announce?(_, %{visibility: visibility}) def announce_visibility(_, %{visibility: visibility})
when visibility in ~w{public unlisted private direct}, when visibility in ~w{public unlisted private direct local},
do: visibility in ~w(public unlisted) do: visibility
defp public_announce?(object, _) do def announce_visibility(object, _), do: Visibility.get_visibility(object)
Visibility.public?(object)
end
@spec get_visibility(map(), map() | nil, Participation.t() | nil) :: @spec get_visibility(map(), map() | nil, Participation.t() | nil) ::
{String.t() | nil, String.t() | nil} {String.t() | nil, String.t() | nil}

View file

@ -75,48 +75,70 @@ defmodule Pleroma.Web.CommonAPI.Utils do
{Enum.map(participation.recipients, & &1.ap_id), []} {Enum.map(participation.recipients, & &1.ap_id), []}
end end
def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public", "local"] do def get_to_and_cc(%{visibility: visibility} = draft) do
to = # If the OP is a DM already, add the implicit actor
case visibility do mentions =
"public" -> [Pleroma.Constants.as_public() | draft.mentions] if visibility == "direct" && draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do
"local" -> [Utils.as_local_public() | draft.mentions] Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions])
else
draft.mentions
end end
cc = [draft.user.follower_address] get_to_and_cc_for_visibility(
visibility,
if draft.in_reply_to do draft.user.follower_address,
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc} draft.in_reply_to && draft.in_reply_to.data["actor"],
else mentions
{to, cc} )
end
end end
def get_to_and_cc(%{visibility: "unlisted"} = draft) do def get_to_and_cc_for_visibility("public", follower_collection, parent_actor, mentions) do
to = [draft.user.follower_address | draft.mentions] scope_addr = Pleroma.Constants.as_public()
cc = [Pleroma.Constants.as_public()]
if draft.in_reply_to do to =
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc} if parent_actor,
else do: Enum.uniq([parent_actor, scope_addr | mentions]),
{to, cc} else: [scope_addr | mentions]
end
{to, [follower_collection]}
end end
def get_to_and_cc(%{visibility: "private"} = draft) do def get_to_and_cc_for_visibility("local", follower_collection, parent_actor, mentions) do
{to, cc} = get_to_and_cc(struct(draft, visibility: "direct")) recipients =
{[draft.user.follower_address | to], cc} if parent_actor,
do: Enum.uniq([parent_actor | mentions]),
else: mentions
to = [
Utils.as_local_public()
| Enum.filter(recipients, fn addr ->
String.starts_with?(addr, Pleroma.Web.Endpoint.url() <> "/")
end)
]
{to, [follower_collection]}
end end
def get_to_and_cc(%{visibility: "direct"} = draft) do def get_to_and_cc_for_visibility("unlisted", follower_collection, parent_actor, mentions) do
# If the OP is a DM already, add the implicit actor. to =
if draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do if parent_actor,
{Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []} do: Enum.uniq([parent_actor, follower_collection | mentions]),
else else: [follower_collection | mentions]
{draft.mentions, []}
end {to, [Pleroma.Constants.as_public()]}
end end
def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []} def get_to_and_cc_for_visibility("private", follower_collection, _, mentions) do
{[follower_collection | mentions], []}
end
def get_to_and_cc_for_visibility("direct", _, _, mentions) do
{mentions, []}
end
def get_to_and_cc_for_visibility({:list, _}, _, _, mentions) do
{mentions, []}
end
def get_addressed_users(_, to) when is_list(to) do def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to) User.get_ap_ids_by_nicknames(to)

View file

@ -86,23 +86,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidationTest do
object = Object.normalize(post_activity, fetch: false) object = Object.normalize(post_activity, fetch: false)
# Another user can't announce it # Another user can't announce it
{:ok, announce, []} = Builder.announce(announcer, object, public: false) {:ok, announce, []} = Builder.announce(announcer, object, visibility: "private")
{:error, cng} = ObjectValidator.validate(announce, []) {:error, cng} = ObjectValidator.validate(announce, [])
assert {:actor, {"can not announce this object", []}} in cng.errors assert {:actor, {"can not announce this object", []}} in cng.errors
# The actor of the object can announce it # The actor of the object can announce it with a restrictive scope
{:ok, announce, []} = Builder.announce(user, object, public: false) {:ok, announce, []} = Builder.announce(user, object, visibility: "private")
assert {:ok, _, _} = ObjectValidator.validate(announce, [])
{:ok, announce, []} = Builder.announce(user, object, visibility: "direct")
assert {:ok, _, _} = ObjectValidator.validate(announce, []) assert {:ok, _, _} = ObjectValidator.validate(announce, [])
# The actor of the object can not announce it publicly # The actor of the object can not announce it publicly
{:ok, announce, []} = Builder.announce(user, object, public: true) {:ok, announce, []} = Builder.announce(user, object, visibility: "public")
{:error, cng1} = ObjectValidator.validate(announce, [])
{:error, cng} = ObjectValidator.validate(announce, []) {:ok, announce, []} = Builder.announce(user, object, visibility: "unlisted")
{:error, cng2} = ObjectValidator.validate(announce, [])
assert {:actor, {"can not announce this object publicly", []}} in cng.errors {:ok, announce, []} = Builder.announce(user, object, visibility: "local")
{:error, cng3} = ObjectValidator.validate(announce, [])
for cng <- [cng1, cng2, cng3] do
assert {:actor, {"can not announce this object publicly", []}} in cng.errors
end
end end
end end
end end

View file

@ -784,13 +784,15 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
{:ok, post} = CommonAPI.post(poster, %{status: "hey"}) {:ok, post} = CommonAPI.post(poster, %{status: "hey"})
{:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) {:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"})
{:ok, announce_data, _meta} = Builder.announce(user, post.object, public: true) {:ok, announce_data, _meta} = Builder.announce(user, post.object, visibility: "public")
{:ok, private_announce_data, _meta} = {:ok, private_announce_data, _meta} =
Builder.announce(user, private_post.object, public: false) Builder.announce(user, private_post.object, visibility: "private")
{:ok, relay_announce_data, _meta} = {:ok, relay_announce_data, _meta} =
Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, public: true) Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object,
visibility: "public"
)
{:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true) {:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true)
{:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true) {:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true)