From 7a58ddfa486048d17fa653662914e0f02d11fadb Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 5 Nov 2023 18:49:31 -0500 Subject: [PATCH 1/6] Allow local user to have group actor type https://git.pleroma.social/pleroma/pleroma/-/issues/3205 --- lib/pleroma/constants.ex | 8 ++++++++ lib/pleroma/user.ex | 3 ++- .../web/mastodon_api/views/account_view.ex | 10 +++++++++- .../web/mastodon_api/update_credentials_test.exs | 16 ++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 77bc4bfac..d814b4931 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -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 # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1 const(mime_regex, diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 10dafbe6f..0fd1b6365 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -39,6 +39,7 @@ defmodule Pleroma.User do alias Pleroma.Workers.BackgroundWorker require Logger + require Pleroma.Constants @type t :: %__MODULE__{} @type account_status :: @@ -579,7 +580,7 @@ defmodule Pleroma.User do |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: bio_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_emoji() |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)}) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 237de3055..e7c555eb2 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -212,7 +212,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do do: user.follower_count, else: 0 - bot = user.actor_type == "Service" + bot = is_bot?(user) emojis = Enum.map(user.emoji, fn {shortcode, raw_url} -> @@ -468,4 +468,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp image_url(%{"url" => [%{"href" => href} | _]}), do: href 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 diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index fc8b79536..cf26cd9a6 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -732,4 +732,20 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do assert account["source"]["pleroma"]["actor_type"] == "Person" 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 From 5459bbc1efabc373e5619990be8d811df072e5e6 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 6 Nov 2023 19:59:05 -0500 Subject: [PATCH 2/6] Allow group actors to boost posts --- lib/pleroma/web/activity_pub/activity_pub.ex | 1 + lib/pleroma/web/activity_pub/side_effects.ex | 2 + lib/pleroma/web/activity_pub/utils.ex | 17 +++++++ test/pleroma/web/common_api_test.exs | 49 ++++++++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 32d1a1037..3b0140d96 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -319,6 +319,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do {:ok, _actor} <- update_last_status_at_if_public(actor, activity), _ <- notify_and_stream(activity), :ok <- maybe_schedule_poll_notifications(activity), + :ok <- maybe_handle_group_posts(activity), :ok <- maybe_federate(activity) do {:ok, activity} else diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 10f268f05..59b19180d 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -233,6 +233,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do Pleroma.Search.add_to_index(Map.put(activity, :object, object)) + Utils.maybe_handle_group_posts(activity) + meta = meta |> add_notifications(notifications) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index b32f19740..e4edbd5ee 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -935,4 +935,21 @@ defmodule Pleroma.Web.ActivityPub.Utils do |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) |> Repo.all() end + + def maybe_handle_group_posts(activity) do + mentions = + activity.data["to"] + |> Enum.filter(&(&1 != activity.actor)) + + mentioned_local_groups = + User.get_all_by_ap_id(mentions) + |> Enum.filter(&(&1.actor_type == "Group" and &1.local)) + + mentioned_local_groups + |> Enum.each(fn group -> + Pleroma.Web.CommonAPI.repeat(activity.id, group) + end) + + :ok + end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 98b922b52..275a633c2 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -26,8 +26,15 @@ defmodule Pleroma.Web.CommonAPITest do import Mox import Pleroma.Factory + require Pleroma.Activity.Queries 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 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok @@ -1835,4 +1842,46 @@ defmodule Pleroma.Web.CommonAPITest do assert Map.has_key?(updated_object.data, "updated") 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 + end end From 5f5533b88a66bdd3547eb5e29f29191093457052 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 7 Nov 2023 21:06:37 -0500 Subject: [PATCH 3/6] Test group actor behaviour in SideEffects --- .../web/activity_pub/side_effects_test.exs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index 6820e23d0..2b1d414b9 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -17,11 +17,19 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.SideEffects + alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.ActivityDraft import Mock 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 test "it streams out notifications and streams" do author = insert(:user, local: true) @@ -915,4 +923,66 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do assert User.get_follow_state(user, followed, nil) == nil 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 + end end From e34a975dd946cc609638d85c20a57e2bfed6ebc7 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 7 Nov 2023 21:16:24 -0500 Subject: [PATCH 4/6] Do not boost if group is blocking poster --- lib/pleroma/web/activity_pub/utils.ex | 8 +++++++- .../web/activity_pub/side_effects_test.exs | 19 +++++++++++++++++++ test/pleroma/web/common_api_test.exs | 8 ++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index e4edbd5ee..e2fc2640d 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -937,13 +937,19 @@ defmodule Pleroma.Web.ActivityPub.Utils do 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(&(&1.actor_type == "Group" and &1.local)) + |> 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 -> diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index 2b1d414b9..94cc80b76 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -984,5 +984,24 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do 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 diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 275a633c2..f002172c5 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -1883,5 +1883,13 @@ defmodule Pleroma.Web.CommonAPITest do 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 From e9d2fadd8e4adfbec3dc3026bc90b2405039d192 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 7 Nov 2023 21:17:15 -0500 Subject: [PATCH 5/6] Add changelog for group actors --- changelog.d/group-actor.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/group-actor.add diff --git a/changelog.d/group-actor.add b/changelog.d/group-actor.add new file mode 100644 index 000000000..2f614b3d8 --- /dev/null +++ b/changelog.d/group-actor.add @@ -0,0 +1 @@ +Implement group actors From b273025fd7612eb869c5a14a8cca81cbc45bad42 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 27 Dec 2023 12:21:05 -0500 Subject: [PATCH 6/6] Add pleroma:group_actors to instance features --- lib/pleroma/web/mastodon_api/views/instance_view.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index f95b5360a..e514d0e91 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -125,7 +125,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do if Config.get([:instance, :profile_directory]) do "profile_directory" end, - "pleroma:get:main/ostatus" + "pleroma:get:main/ostatus", + "pleroma:group_actors" ] |> Enum.filter(& &1) end