From 78dc7613bcd2a6a54edf4faaf95035de3ed2d393 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 29 Jul 2021 17:48:28 +0200 Subject: [PATCH] Refactor Mobilizon.Federation.ActivityPub.Audience and add tests Signed-off-by: Thomas Citharel --- lib/federation/activity_pub/activity_pub.ex | 10 +- lib/federation/activity_pub/audience.ex | 223 ++++++------ lib/federation/activity_pub/types/actors.ex | 4 +- lib/federation/activity_pub/types/comments.ex | 6 +- .../activity_pub/types/discussions.ex | 6 +- lib/federation/activity_pub/types/events.ex | 8 +- lib/federation/activity_pub/types/posts.ex | 2 +- .../activity_stream/converter/post.ex | 2 +- .../federation/activity_pub/audience_test.exs | 319 ++++++++++++++++++ 9 files changed, 444 insertions(+), 136 deletions(-) create mode 100644 test/federation/activity_pub/audience_test.exs diff --git a/lib/federation/activity_pub/activity_pub.ex b/lib/federation/activity_pub/activity_pub.ex index 0e11c056a..4dadd4aaf 100644 --- a/lib/federation/activity_pub/activity_pub.ex +++ b/lib/federation/activity_pub/activity_pub.ex @@ -426,7 +426,7 @@ defmodule Mobilizon.Federation.ActivityPub do "id" => "#{Endpoint.url()}/leave/event/#{participant.id}" }, audience <- - Audience.calculate_to_and_cc_from_mentions(participant), + Audience.get_audience(participant), {:ok, activity} <- create_activity(Map.merge(leave_data, audience), local), :ok <- maybe_federate(activity) do {:ok, activity, participant} @@ -803,7 +803,7 @@ defmodule Mobilizon.Federation.ActivityPub do Scheduler.trigger_notifications_for_participant(participant), participant_as_data <- Convertible.model_to_as(participant), audience <- - Audience.calculate_to_and_cc_from_mentions(participant), + Audience.get_audience(participant), update_data <- make_accept_join_data( participant_as_data, @@ -837,7 +837,7 @@ defmodule Mobilizon.Federation.ActivityPub do ), member_as_data <- Convertible.model_to_as(member), audience <- - Audience.calculate_to_and_cc_from_mentions(member), + Audience.get_audience(member), update_data <- make_accept_join_data( member_as_data, @@ -899,7 +899,7 @@ defmodule Mobilizon.Federation.ActivityPub do participant_as_data <- Convertible.model_to_as(participant), audience <- participant - |> Audience.calculate_to_and_cc_from_mentions() + |> Audience.get_audience() |> Map.merge(additional), reject_data <- %{ "type" => "Reject", @@ -925,7 +925,7 @@ defmodule Mobilizon.Federation.ActivityPub do with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower), follower_as_data <- Convertible.model_to_as(follower), audience <- - follower.actor |> Audience.calculate_to_and_cc_from_mentions() |> Map.merge(additional), + follower.actor |> Audience.get_audience() |> Map.merge(additional), reject_data <- %{ "to" => [follower.actor.url], "type" => "Reject", diff --git a/lib/federation/activity_pub/audience.ex b/lib/federation/activity_pub/audience.ex index f129e59b9..a2ccaf5ca 100644 --- a/lib/federation/activity_pub/audience.ex +++ b/lib/federation/activity_pub/audience.ex @@ -3,18 +3,95 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do Tools for calculating content audience """ - alias Mobilizon.Actors + alias Mobilizon.{Actors, Events, Share} alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Posts.Post - alias Mobilizon.Share alias Mobilizon.Storage.Repo require Logger @ap_public "https://www.w3.org/ns/activitystreams#Public" + @type audience :: %{required(String.t()) => list(String.t())} + + @doc """ + Get audience for an entity + """ + @spec get_audience(Entity.t()) :: audience() + def get_audience(%Event{} = event) do + extract_actors_from_event(event) + end + + def get_audience(%Post{draft: true} = post) do + get_audience(%Post{post | visibility: :private, draft: false}) + end + + def get_audience(%Post{attributed_to: %Actor{} = group, visibility: visibility}) do + {to, cc} = get_to_and_cc(group, [], visibility) + %{"to" => to, "cc" => cc} + end + + def get_audience(%Discussion{actor: actor}) do + %{"to" => maybe_add_group_members([], actor), "cc" => []} + end + + def get_audience(%Comment{discussion: %Discussion{} = discussion}) do + get_audience(discussion) + end + + def get_audience(%Comment{ + mentions: mentions, + actor: %Actor{} = actor, + visibility: visibility, + in_reply_to_comment: in_reply_to_comment, + event: event, + origin_comment: origin_comment, + url: url + }) do + with {to, cc} <- + extract_actors_from_mentions(mentions, actor, visibility), + {to, cc} <- {Enum.uniq(to ++ add_in_reply_to(in_reply_to_comment)), cc}, + {to, cc} <- {Enum.uniq(to ++ add_event_author(event)), cc}, + {to, cc} <- + {to, + Enum.uniq( + cc ++ + add_comments_authors([origin_comment]) ++ + add_shares_actors_followers(url) + )} do + %{"to" => to, "cc" => cc} + end + end + + def get_audience(%Participant{} = participant) do + event = Events.get_event_with_preload!(participant.event_id) + + actor_participants_urls = + event.id + |> Mobilizon.Events.list_actors_participants_for_event() + |> Enum.map(& &1.url) + + %{ + "to" => [participant.actor.url, group_or_organizer_event(event).url], + "cc" => actor_participants_urls + } + end + + def get_audience(%Member{} = member) do + %{"to" => [member.parent.url, member.parent.members_url], "cc" => []} + end + + def get_audience(%Actor{} = actor) do + %{ + "to" => [@ap_public], + "cc" => + maybe_add_group_members([actor.followers_url], actor) ++ + add_actors_that_had_our_content(actor.id) + } + end + @doc """ Determines the full audience based on mentions for an audience @@ -39,6 +116,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do to = [@ap_public | mentions] cc = [actor.followers_url] + cc = maybe_add_group_members(cc, actor) + {to, cc} end @@ -47,13 +126,18 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do to = [actor.followers_url | mentions] cc = [@ap_public] + to = maybe_add_group_members(to, actor) + {to, cc} end @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} def get_to_and_cc(%Actor{} = actor, mentions, :private) do {to, cc} = get_to_and_cc(actor, mentions, :direct) - {[actor.followers_url | to], cc} + + to = maybe_add_group_members(to, actor) + + {to, cc} end @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} @@ -65,125 +149,24 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do {mentions, []} end + @spec maybe_add_group_members(List.t(), Actor.t()) :: List.t() + defp maybe_add_group_members(collection, %Actor{type: :Group, members_url: members_url}) do + [members_url | collection] + end + + defp maybe_add_group_members(collection, %Actor{type: _}), do: collection + def get_addressed_actors(mentioned_users, _), do: mentioned_users - def calculate_to_and_cc_from_mentions( - %Comment{discussion: %Discussion{actor_id: actor_id}} = _comment - ) do - with %Actor{type: :Group, members_url: members_url} <- Actors.get_actor(actor_id) do - %{"to" => [members_url], "cc" => []} - end - end - - def calculate_to_and_cc_from_mentions(%Comment{} = comment) do - with {to, cc} <- - extract_actors_from_mentions(comment.mentions, comment.actor, comment.visibility), - {to, cc} <- {Enum.uniq(to ++ add_in_reply_to(comment.in_reply_to_comment)), cc}, - {to, cc} <- {Enum.uniq(to ++ add_event_author(comment.event)), cc}, - {to, cc} <- - {to, - Enum.uniq( - cc ++ - add_comments_authors([comment.origin_comment]) ++ - add_shares_actors_followers(comment.url) - )} do - %{"to" => to, "cc" => cc} - end - end - - def calculate_to_and_cc_from_mentions(%Discussion{actor_id: actor_id}) do - with %Actor{type: :Group, members_url: members_url} <- Actors.get_actor(actor_id) do - %{"to" => [members_url], "cc" => []} - end - end - - def calculate_to_and_cc_from_mentions( - %Event{ - attributed_to: %Actor{members_url: members_url}, - visibility: visibility - } = event - ) do - %{"to" => to, "cc" => cc} = extract_actors_from_event(event) - - case visibility do - :public -> - %{"to" => [@ap_public, members_url] ++ to, "cc" => [] ++ cc} - - :unlisted -> - %{"to" => [members_url] ++ to, "cc" => [@ap_public] ++ cc} - - :private -> - # Private is restricted to only the members - %{"to" => [members_url], "cc" => []} - end - end - - def calculate_to_and_cc_from_mentions(%Event{} = event) do - extract_actors_from_event(event) - end - - def calculate_to_and_cc_from_mentions(%Post{ - attributed_to: %Actor{members_url: members_url, followers_url: followers_url}, - visibility: visibility, - draft: draft - }) do - cond do - # If the post is draft we send it only to members - draft == true -> - %{"to" => [members_url], "cc" => []} - - # If public everyone - visibility == :public -> - %{"to" => [@ap_public, members_url], "cc" => [followers_url]} - - # Otherwise just followers - visibility == :unlisted -> - %{"to" => [followers_url, members_url], "cc" => [@ap_public]} - - visibility == :private -> - # Private is restricted to only the members - %{"to" => [members_url], "cc" => []} - - true -> - %{"to" => [], "cc" => []} - end - end - - def calculate_to_and_cc_from_mentions(%Participant{} = participant) do - participant = Repo.preload(participant, [:actor, :event]) - - actor_participants_urls = - participant.event.id - |> Mobilizon.Events.list_actors_participants_for_event() - |> Enum.map(& &1.url) - - %{"to" => [participant.actor.url], "cc" => actor_participants_urls} - end - - def calculate_to_and_cc_from_mentions(%Member{} = member) do - member = Repo.preload(member, [:parent]) - - %{"to" => [member.parent.members_url], "cc" => []} - end - - def calculate_to_and_cc_from_mentions(%Actor{} = actor) do - %{ - "to" => [@ap_public], - "cc" => [actor.followers_url] ++ add_actors_that_had_our_content(actor.id) - } - end - defp add_in_reply_to(%Comment{actor: %Actor{url: url}} = _comment), do: [url] defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url] defp add_in_reply_to(_), do: [] - defp add_event_author(nil), do: [] - defp add_event_author(%Event{} = event) do [Repo.preload(event, [:organizer_actor]).organizer_actor.url] end - defp add_comment_author(nil), do: nil + defp add_event_author(_), do: [] defp add_comment_author(%Comment{} = comment) do case Repo.preload(comment, [:actor]) do @@ -195,6 +178,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do end end + defp add_comment_author(_), do: nil + defp add_comments_authors(comments) do authors = comments @@ -208,8 +193,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do defp add_shares_actors_followers(uri) do uri |> Share.get_actors_by_share_uri() - |> Enum.map(&Actors.list_followers_actors_for_actor/1) - |> List.flatten() |> Enum.map(& &1.url) |> Enum.uniq() end @@ -217,8 +200,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do defp add_actors_that_had_our_content(actor_id) do actor_id |> Share.get_actors_by_owner_actor_id() - |> Enum.map(&Actors.list_followers_actors_for_actor/1) - |> List.flatten() |> Enum.map(& &1.url) |> Enum.uniq() end @@ -241,7 +222,11 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do defp extract_actors_from_event(%Event{} = event) do with {to, cc} <- - extract_actors_from_mentions(event.mentions, event.organizer_actor, event.visibility), + extract_actors_from_mentions( + event.mentions, + group_or_organizer_event(event), + event.visibility + ), {to, cc} <- {to, Enum.uniq( @@ -253,4 +238,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do %{"to" => [], "cc" => []} end end + + @spec group_or_organizer_event(Event.t()) :: Actor.t() + defp group_or_organizer_event(%Event{attributed_to: %Actor{} = group}), do: group + defp group_or_organizer_event(%Event{organizer_actor: %Actor{} = actor}), do: actor end diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex index 3df8e275f..f7d7d4d95 100644 --- a/lib/federation/activity_pub/types/actors.ex +++ b/lib/federation/activity_pub/types/actors.ex @@ -47,7 +47,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do actor_as_data <- Convertible.model_to_as(new_actor), {:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"), audience <- - Audience.calculate_to_and_cc_from_mentions(new_actor), + Audience.get_audience(new_actor), additional <- Map.merge(additional, %{"actor" => old_actor.url}), update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do {:ok, new_actor, update_data} @@ -142,7 +142,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do "object" => group.url }, audience <- - Audience.calculate_to_and_cc_from_mentions(member) do + Audience.get_audience(member) do approve_if_default_role_is_member( group, actor, diff --git a/lib/federation/activity_pub/types/comments.ex b/lib/federation/activity_pub/types/comments.ex index ea8de5ba5..345d7d7e5 100644 --- a/lib/federation/activity_pub/types/comments.ex +++ b/lib/federation/activity_pub/types/comments.ex @@ -32,7 +32,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do :ok <- maybe_publish_graphql_subscription(discussion_id), comment_as_data <- Convertible.model_to_as(comment), audience <- - Audience.calculate_to_and_cc_from_mentions(comment), + Audience.get_audience(comment), create_data <- make_create_data(comment_as_data, Map.merge(audience, additional)) do {:ok, comment, create_data} @@ -47,7 +47,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do {:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"), comment_as_data <- Convertible.model_to_as(new_comment), audience <- - Audience.calculate_to_and_cc_from_mentions(new_comment), + Audience.get_audience(new_comment), update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do {:ok, new_comment, update_data} else @@ -79,7 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do force_deletion = Map.get(options, :force, false) with audience <- - Audience.calculate_to_and_cc_from_mentions(comment), + Audience.get_audience(comment), {:ok, %Comment{} = updated_comment} <- Discussions.delete_comment(comment, force: force_deletion), {:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"), diff --git a/lib/federation/activity_pub/types/discussions.ex b/lib/federation/activity_pub/types/discussions.ex index f282a4305..1fb013872 100644 --- a/lib/federation/activity_pub/types/discussions.ex +++ b/lib/federation/activity_pub/types/discussions.ex @@ -31,7 +31,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do :ok <- maybe_publish_graphql_subscription(discussion), comment_as_data <- Convertible.model_to_as(last_comment), audience <- - Audience.calculate_to_and_cc_from_mentions(discussion), + Audience.get_audience(discussion), create_data <- make_create_data(comment_as_data, Map.merge(audience, additional)) do {:ok, discussion, create_data} @@ -48,7 +48,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do DiscussionActivity.insert_activity(discussion, subject: "discussion_created"), discussion_as_data <- Convertible.model_to_as(discussion), audience <- - Audience.calculate_to_and_cc_from_mentions(discussion), + Audience.get_audience(discussion), create_data <- make_create_data(discussion_as_data, Map.merge(audience, additional)) do {:ok, discussion, create_data} @@ -68,7 +68,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do {:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"), discussion_as_data <- Convertible.model_to_as(new_discussion), audience <- - Audience.calculate_to_and_cc_from_mentions(new_discussion), + Audience.get_audience(new_discussion), update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do {:ok, new_discussion, update_data} else diff --git a/lib/federation/activity_pub/types/events.ex b/lib/federation/activity_pub/types/events.ex index 917c0769d..df556d89b 100644 --- a/lib/federation/activity_pub/types/events.ex +++ b/lib/federation/activity_pub/types/events.ex @@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do EventActivity.insert_activity(event, subject: "event_created"), event_as_data <- Convertible.model_to_as(event), audience <- - Audience.calculate_to_and_cc_from_mentions(event), + Audience.get_audience(event), create_data <- make_create_data(event_as_data, Map.merge(audience, additional)) do {:ok, event, create_data} @@ -46,7 +46,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do {:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"), event_as_data <- Convertible.model_to_as(new_event), audience <- - Audience.calculate_to_and_cc_from_mentions(new_event), + Audience.get_audience(new_event), update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do {:ok, new_event, update_data} else @@ -69,7 +69,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do } with audience <- - Audience.calculate_to_and_cc_from_mentions(event), + Audience.get_audience(event), {:ok, %Event{} = event} <- EventsManager.delete_event(event), {:ok, _} <- EventActivity.insert_activity(event, subject: "event_deleted"), @@ -124,7 +124,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do }), join_data <- Convertible.model_to_as(participant), audience <- - Audience.calculate_to_and_cc_from_mentions(participant) do + Audience.get_audience(participant) do approve_if_default_role_is_participant( event, Map.merge(join_data, audience), diff --git a/lib/federation/activity_pub/types/posts.ex b/lib/federation/activity_pub/types/posts.ex index 5f90f4c0b..c6a5bab3d 100644 --- a/lib/federation/activity_pub/types/posts.ex +++ b/lib/federation/activity_pub/types/posts.ex @@ -47,7 +47,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do post_as_data <- Convertible.model_to_as(%{post | attributed_to: group, author: creator}), audience <- - Audience.calculate_to_and_cc_from_mentions(post) do + Audience.get_audience(post) do update_data = make_update_data(post_as_data, Map.merge(audience, additional)) {:ok, post, update_data} diff --git a/lib/federation/activity_stream/converter/post.ex b/lib/federation/activity_stream/converter/post.ex index 4f09c4285..0cb0dfee4 100644 --- a/lib/federation/activity_stream/converter/post.ex +++ b/lib/federation/activity_stream/converter/post.ex @@ -41,7 +41,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do } } = post ) do - audience = Audience.calculate_to_and_cc_from_mentions(post) + audience = Audience.get_audience(post) %{ "type" => "Article", diff --git a/test/federation/activity_pub/audience_test.exs b/test/federation/activity_pub/audience_test.exs new file mode 100644 index 000000000..f00ced414 --- /dev/null +++ b/test/federation/activity_pub/audience_test.exs @@ -0,0 +1,319 @@ +defmodule Mobilizon.Federation.ActivityPub.AudienceTest do + use Mobilizon.DataCase + + import Mobilizon.Factory + + alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Mention + alias Mobilizon.Discussions.{Comment, Discussion} + alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Federation.ActivityPub.Audience + alias Mobilizon.Posts.Post + alias Mobilizon.Storage.Repo + + @ap_public "https://www.w3.org/ns/activitystreams#Public" + + describe "get audience for an event created from a profile" do + test "when the event is public" do + %Event{} = event = insert(:event) + event = Repo.preload(event, [:comments]) + + assert %{"cc" => [event.organizer_actor.followers_url], "to" => [@ap_public]} == + Audience.get_audience(event) + end + + test "when the event is unlisted" do + %Event{} = event = insert(:event, visibility: :unlisted) + event = Repo.preload(event, [:comments]) + + assert %{"cc" => [@ap_public], "to" => [event.organizer_actor.followers_url]} == + Audience.get_audience(event) + end + + test "when the event is unlisted and mentions some actors" do + %Actor{id: mentionned_actor_id, url: mentionned_actor_url} = + insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Event{} = event = insert(:event, visibility: :unlisted) + event = Repo.preload(event, [:comments]) + mentions = [%Mention{actor_id: mentionned_actor_id}] + event = %Event{event | mentions: mentions} + + assert %{ + "cc" => [@ap_public], + "to" => [event.organizer_actor.followers_url, mentionned_actor_url] + } == + Audience.get_audience(event) + end + + test "with interactions" do + %Actor{} = interactor = insert(:actor) + + %Event{} = event = insert(:event) + + insert(:share, owner_actor: event.organizer_actor, actor: interactor, uri: event.url) + + event = Repo.preload(event, [:comments]) + + assert %{ + "cc" => [event.organizer_actor.followers_url, interactor.url], + "to" => [@ap_public] + } == + Audience.get_audience(event) + end + end + + describe "get audience for an event created from a group member" do + test "when the event is public" do + %Actor{} = actor = insert(:actor) + + %Actor{ + followers_url: followers_url, + members_url: members_url + } = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Event{} = event = insert(:event, attributed_to: group, organizer_actor: actor) + event = Repo.preload(event, [:comments]) + + assert %{ + "cc" => [members_url, followers_url], + "to" => [@ap_public] + } == + Audience.get_audience(event) + end + + test "when the event is unlisted" do + %Actor{} = actor = insert(:actor) + + %Actor{ + followers_url: followers_url, + members_url: members_url + } = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Event{} = + event = + insert(:event, visibility: :unlisted, attributed_to: group, organizer_actor: actor) + + event = Repo.preload(event, [:comments]) + + assert %{ + "cc" => [@ap_public], + "to" => [members_url, followers_url] + } == + Audience.get_audience(event) + end + + test "when the event is unlisted and mentions some actors" do + %Actor{id: mentionned_actor_id, url: mentionned_actor_url} = + insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Actor{} = actor = insert(:actor) + + %Actor{ + followers_url: followers_url, + members_url: members_url + } = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@a_group") + + %Event{} = + event = + insert(:event, visibility: :unlisted, attributed_to: group, organizer_actor: actor) + + event = Repo.preload(event, [:comments]) + mentions = [%Mention{actor_id: mentionned_actor_id}] + event = %Event{event | mentions: mentions} + + assert %{ + "cc" => [@ap_public], + "to" => [members_url, followers_url, mentionned_actor_url] + } == + Audience.get_audience(event) + end + end + + describe "get audience for a post" do + test "when it's public" do + %Actor{ + followers_url: followers_url, + members_url: members_url + } = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Post{} = post = insert(:post, attributed_to: group) + + assert %{"to" => [@ap_public], "cc" => [members_url, followers_url]} == + Audience.get_audience(post) + end + + test "when it's unlisted" do + %Actor{ + followers_url: followers_url, + members_url: members_url + } = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Post{} = post = insert(:post, attributed_to: group, visibility: :unlisted) + + assert %{"to" => [members_url, followers_url], "cc" => [@ap_public]} == + Audience.get_audience(post) + end + + test "when it's private" do + %Actor{ + members_url: members_url + } = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Post{} = post = insert(:post, attributed_to: group, visibility: :private) + + assert %{"to" => [members_url], "cc" => []} == + Audience.get_audience(post) + end + + test "when it's still a draft" do + %Actor{ + members_url: members_url + } = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Post{} = post = insert(:post, attributed_to: group, draft: true) + + assert %{"to" => [members_url], "cc" => []} == + Audience.get_audience(post) + end + end + + describe "get audience for a discussion" do + test "basic" do + %Actor{ + members_url: members_url + } = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Discussion{} = discussion = insert(:discussion, actor: group) + + assert %{"to" => [members_url], "cc" => []} == + Audience.get_audience(discussion) + end + end + + describe "get audience for a comment" do + test "basic" do + %Actor{id: mentionned_actor_id, url: mentionned_actor_url} = + insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Comment{} = comment = insert(:comment) + mentions = [%Mention{actor_id: mentionned_actor_id}] + comment = %Comment{comment | mentions: mentions} + + assert %{ + "cc" => [comment.actor.followers_url], + "to" => [@ap_public, mentionned_actor_url, comment.event.organizer_actor.url] + } == + Audience.get_audience(comment) + end + + test "in reply to other comments" do + %Actor{id: mentionned_actor_id, url: mentionned_actor_url} = + insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Comment{} = original_comment = insert(:comment) + + %Comment{} = + reply_comment = + insert(:comment, in_reply_to_comment: original_comment, origin_comment: original_comment) + + %Comment{} = + comment = + insert(:comment, in_reply_to_comment: reply_comment, origin_comment: original_comment) + + mentions = [%Mention{actor_id: mentionned_actor_id}] + comment = %Comment{comment | mentions: mentions} + + assert %{ + "cc" => [comment.actor.followers_url, original_comment.actor.url], + "to" => [ + @ap_public, + mentionned_actor_url, + reply_comment.actor.url, + comment.event.organizer_actor.url + ] + } == + Audience.get_audience(comment) + end + + test "part of a discussion" do + %Actor{ + members_url: members_url + } = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Discussion{} = discussion = insert(:discussion, actor: group) + %Comment{} = comment = insert(:comment, discussion: discussion) + + assert %{"to" => [members_url], "cc" => []} == + Audience.get_audience(comment) + end + end + + describe "participant" do + test "basic" do + %Event{} = event = insert(:event) + %Participant{} = participant2 = insert(:participant, event: event) + %Participant{} = participant = insert(:participant, event: event) + + assert %{ + "to" => [participant.actor.url, participant.event.organizer_actor.url], + "cc" => [participant2.actor.url, participant.actor.url] + } == Audience.get_audience(participant) + end + + test "to a group event" do + %Actor{} = + group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone") + + %Event{} = event = insert(:event, attributed_to: group) + %Participant{} = participant2 = insert(:participant, event: event) + %Participant{} = participant = insert(:participant, event: event) + + assert %{ + "to" => [participant.actor.url, participant.event.attributed_to.url], + "cc" => [participant2.actor.url, participant.actor.url] + } == Audience.get_audience(participant) + end + end + + describe "member" do + test "basic" do + %Member{} = member = insert(:member) + + assert %{"to" => [member.parent.url, member.parent.members_url], "cc" => []} == + Audience.get_audience(member) + end + end + + describe "actor" do + test "basic" do + %Actor{followers_url: followers_url} = actor = insert(:actor) + + assert %{"to" => [@ap_public], "cc" => [followers_url]} == + Audience.get_audience(actor) + end + + test "group" do + %Actor{followers_url: followers_url, members_url: members_url, type: :Group} = + group = insert(:group) + + assert %{"to" => [@ap_public], "cc" => [members_url, followers_url]} == + Audience.get_audience(group) + end + + test "with interactions" do + %Actor{followers_url: followers_url, members_url: members_url, type: :Group} = + group = insert(:group) + + %Actor{} = interactor = insert(:actor) + + insert(:share, owner_actor: group, actor: interactor) + + assert %{ + "to" => [@ap_public], + "cc" => [members_url, followers_url, interactor.url] + } == + Audience.get_audience(group) + end + end +end