Merge branch 'tusooa/3205-group-actor' into 'develop'
Implement group actors See merge request pleroma/pleroma!3969
This commit is contained in:
commit
ddc321a094
|
@ -0,0 +1 @@
|
||||||
|
Implement group actors
|
|
@ -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,
|
||||||
|
|
|
@ -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)})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue