Merge branch 'tusooa/3205-group-actor' into 'develop'

Implement group actors

See merge request pleroma/pleroma!3969
This commit is contained in:
Haelwenn 2023-12-28 10:46:53 +00:00
commit ddc321a094
11 changed files with 210 additions and 3 deletions

View File

@ -0,0 +1 @@
Implement group actors

View File

@ -76,6 +76,14 @@ defmodule Pleroma.Constants do
] ]
) )
const(allowed_user_actor_types,
do: [
"Person",
"Service",
"Group"
]
)
# basic regex, just there to weed out potential mistakes # basic regex, just there to weed out potential mistakes
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1 # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
const(mime_regex, const(mime_regex,

View File

@ -39,6 +39,7 @@ defmodule Pleroma.User do
alias Pleroma.Workers.BackgroundWorker alias Pleroma.Workers.BackgroundWorker
require Logger require Logger
require Pleroma.Constants
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@type account_status :: @type account_status ::
@ -579,7 +580,7 @@ def update_changeset(struct, params \\ %{}) do
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit) |> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit) |> validate_length(:name, min: 1, max: name_limit)
|> validate_inclusion(:actor_type, ["Person", "Service"]) |> validate_inclusion(:actor_type, Pleroma.Constants.allowed_user_actor_types())
|> put_fields() |> put_fields()
|> put_emoji() |> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)}) |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})

View File

@ -319,6 +319,7 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param
{:ok, _actor} <- update_last_status_at_if_public(actor, activity), {:ok, _actor} <- update_last_status_at_if_public(actor, activity),
_ <- notify_and_stream(activity), _ <- notify_and_stream(activity),
:ok <- maybe_schedule_poll_notifications(activity), :ok <- maybe_schedule_poll_notifications(activity),
:ok <- maybe_handle_group_posts(activity),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else else

View File

@ -233,6 +233,8 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
Pleroma.Search.add_to_index(Map.put(activity, :object, object)) Pleroma.Search.add_to_index(Map.put(activity, :object, object))
Utils.maybe_handle_group_posts(activity)
meta = meta =
meta meta
|> add_notifications(notifications) |> add_notifications(notifications)

View File

@ -935,4 +935,27 @@ def get_existing_votes(actor, %{data: %{"id" => id}}) do
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|> Repo.all() |> Repo.all()
end end
def maybe_handle_group_posts(activity) do
poster = User.get_cached_by_ap_id(activity.actor)
mentions =
activity.data["to"]
|> Enum.filter(&(&1 != activity.actor))
mentioned_local_groups =
User.get_all_by_ap_id(mentions)
|> Enum.filter(fn user ->
user.actor_type == "Group" and
user.local and
not User.blocks?(user, poster)
end)
mentioned_local_groups
|> Enum.each(fn group ->
Pleroma.Web.CommonAPI.repeat(activity.id, group)
end)
:ok
end
end end

View File

@ -212,7 +212,7 @@ defp do_render("show.json", %{user: user} = opts) do
do: user.follower_count, do: user.follower_count,
else: 0 else: 0
bot = user.actor_type == "Service" bot = is_bot?(user)
emojis = emojis =
Enum.map(user.emoji, fn {shortcode, raw_url} -> Enum.map(user.emoji, fn {shortcode, raw_url} ->
@ -468,4 +468,12 @@ defp maybe_show_birthday(data, _, _) do
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil defp image_url(_), do: nil
defp is_bot?(user) do
# Because older and/or Mastodon clients may not recognize a Group actor properly,
# and currently the group actor can only boost things, we should let these clients
# think groups are bots.
# See https://git.pleroma.social/pleroma/pleroma-meta/-/issues/14
user.actor_type == "Service" || user.actor_type == "Group"
end
end end

View File

@ -125,7 +125,8 @@ def features do
if Config.get([:instance, :profile_directory]) do if Config.get([:instance, :profile_directory]) do
"profile_directory" "profile_directory"
end, end,
"pleroma:get:main/ostatus" "pleroma:get:main/ostatus",
"pleroma:group_actors"
] ]
|> Enum.filter(& &1) |> Enum.filter(& &1)
end end

View File

@ -17,11 +17,19 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.SideEffects alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.ActivityDraft
import Mock import Mock
import Pleroma.Factory import Pleroma.Factory
defp get_announces_of_object(%{data: %{"id" => id}} = _object) do
Pleroma.Activity.Queries.by_type("Announce")
|> Pleroma.Activity.Queries.by_object_id(id)
|> Pleroma.Repo.all()
end
describe "handle_after_transaction" do describe "handle_after_transaction" do
test "it streams out notifications and streams" do test "it streams out notifications and streams" do
author = insert(:user, local: true) author = insert(:user, local: true)
@ -915,4 +923,85 @@ test "", %{user: user, followed: followed, reject: reject} do
assert User.get_follow_state(user, followed, nil) == nil assert User.get_follow_state(user, followed, nil) == nil
end end
end end
describe "Group actors" do
setup do
poster =
insert(:user,
local: false,
nickname: "poster@example.com",
ap_id: "https://example.com/users/poster"
)
group = insert(:user, actor_type: "Group")
make_create = fn mentioned_users ->
mentions = mentioned_users |> Enum.map(fn u -> "@#{u.nickname}" end) |> Enum.join(" ")
{:ok, draft} = ActivityDraft.create(poster, %{status: "#{mentions} hey"})
create_activity_data =
Utils.make_create_data(draft.changes |> Map.put(:published, nil), %{})
|> put_in(["object", "id"], "https://example.com/object")
|> put_in(["id"], "https://example.com/activity")
assert Enum.all?(mentioned_users, fn u -> u.ap_id in create_activity_data["to"] end)
create_activity_data
end
%{poster: poster, group: group, make_create: make_create}
end
test "group should boost it", %{make_create: make_create, group: group} do
create_activity_data = make_create.([group])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity,
local: false,
object_data: create_activity_data["object"]
)
object = Object.normalize(create_activity, fetch: false)
assert [announce] = get_announces_of_object(object)
assert announce.actor == group.ap_id
end
test "remote group should not boost it", %{make_create: make_create, group: group} do
remote_group =
insert(:user, actor_type: "Group", local: false, nickname: "remotegroup@example.com")
create_activity_data = make_create.([group, remote_group])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity,
local: false,
object_data: create_activity_data["object"]
)
object = Object.normalize(create_activity, fetch: false)
assert [announce] = get_announces_of_object(object)
assert announce.actor == group.ap_id
end
test "group should not boost it if group is blocking poster", %{
make_create: make_create,
group: group,
poster: poster
} do
{:ok, _} = CommonAPI.block(group, poster)
create_activity_data = make_create.([group])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity,
local: false,
object_data: create_activity_data["object"]
)
object = Object.normalize(create_activity, fetch: false)
assert [] = get_announces_of_object(object)
end
end
end end

View File

@ -26,8 +26,15 @@ defmodule Pleroma.Web.CommonAPITest do
import Mox import Mox
import Pleroma.Factory import Pleroma.Factory
require Pleroma.Activity.Queries
require Pleroma.Constants require Pleroma.Constants
defp get_announces_of_object(%{data: %{"id" => id}} = _object) do
Pleroma.Activity.Queries.by_type("Announce")
|> Pleroma.Activity.Queries.by_object_id(id)
|> Pleroma.Repo.all()
end
setup_all do setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok :ok
@ -1835,4 +1842,54 @@ test "respects MRF" do
assert Map.has_key?(updated_object.data, "updated") assert Map.has_key?(updated_object.data, "updated")
end end
end end
describe "Group actors" do
setup do
poster = insert(:user)
group = insert(:user, actor_type: "Group")
other_group = insert(:user, actor_type: "Group")
%{poster: poster, group: group, other_group: other_group}
end
test "it boosts public posts", %{poster: poster, group: group} do
{:ok, post} = CommonAPI.post(poster, %{status: "hey @#{group.nickname}"})
announces = get_announces_of_object(post.object)
assert [_] = announces
end
test "it does not boost private posts", %{poster: poster, group: group} do
{:ok, private_post} =
CommonAPI.post(poster, %{status: "hey @#{group.nickname}", visibility: "private"})
assert [] = get_announces_of_object(private_post.object)
end
test "remote groups do not boost any posts", %{poster: poster} do
remote_group =
insert(:user, actor_type: "Group", local: false, nickname: "remote@example.com")
{:ok, post} = CommonAPI.post(poster, %{status: "hey @#{User.full_nickname(remote_group)}"})
assert remote_group.ap_id in post.data["to"]
announces = get_announces_of_object(post.object)
assert [] = announces
end
test "multiple groups mentioned", %{poster: poster, group: group, other_group: other_group} do
{:ok, post} =
CommonAPI.post(poster, %{status: "hey @#{group.nickname} @#{other_group.nickname}"})
announces = get_announces_of_object(post.object)
assert [_, _] = announces
end
test "it does not boost if group is blocking poster", %{poster: poster, group: group} do
{:ok, _} = CommonAPI.block(group, poster)
{:ok, post} = CommonAPI.post(poster, %{status: "hey @#{group.nickname}"})
announces = get_announces_of_object(post.object)
assert [] = announces
end
end
end end

View File

@ -732,4 +732,20 @@ test "actor_type field has a higher priority than bot", %{conn: conn} do
assert account["source"]["pleroma"]["actor_type"] == "Person" assert account["source"]["pleroma"]["actor_type"] == "Person"
end end
end end
describe "Mark account as group" do
setup do: oauth_access(["write:accounts"])
setup :request_content_type
test "changing actor_type to Group makes account a Group and enables bot indicator for backward compatibility",
%{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{actor_type: "Group"})
|> json_response_and_validate_schema(200)
assert account["bot"]
assert account["source"]["pleroma"]["actor_type"] == "Group"
end
end
end end