defmodule Mobilizon.Federation.ActivityPub.Audience do @moduledoc """ Tools for calculating content audience """ alias Mobilizon.{Actors, Discussions, Events, Share} alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Posts.Post 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() | Participant.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 # Deleted comments are just like tombstones def get_audience(%Comment{deleted_at: deleted_at}) when not is_nil(deleted_at) do %{"to" => [@ap_public], "cc" => []} end def get_audience(%Comment{discussion: %Discussion{id: discussion_id}}) do discussion_id |> Discussions.get_discussion() |> get_audience() 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} <- {to ++ add_in_reply_to(in_reply_to_comment), cc}, {to, cc} <- add_event_organizers(event, to, cc), {to, cc} <- {to, cc ++ add_comments_authors([origin_comment]) ++ add_shares_actors_followers(url)} do %{"to" => Enum.uniq(to), "cc" => Enum.uniq(cc)} end end def get_audience(%Participant{} = participant) do %Event{} = event = Events.get_event_with_preload!(participant.event_id) %Actor{} = organizer = group_or_organizer_event(event) cc = event.id |> Mobilizon.Events.list_actors_participants_for_event() |> Enum.map(& &1.url) |> Enum.filter(&(&1 != participant.actor.url)) |> maybe_add_group_members(organizer) |> maybe_add_followers(organizer) %{ "to" => [participant.actor.url, organizer.url], "cc" => cc } 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 @spec get_to_and_cc(Actor.t(), list(), :direct | :private | :public | :unlisted | {:list, any}) :: {list(), list()} @doc """ Determines the full audience based on mentions for an audience For a public audience: * `to` : the mentioned actors, the eventual actor we're replying to and the public * `cc` : the actor's followers For an unlisted audience: * `to` : the mentioned actors, actor's followers and the eventual actor we're replying to * `cc` : public For a private audience: * `to` : the mentioned actors, actor's followers and the eventual actor we're replying to * `cc` : none For a direct audience: * `to` : the mentioned actors and the eventual actor we're replying to * `cc` : none """ def get_to_and_cc(%Actor{} = actor, mentions, :public) do to = [@ap_public | mentions] cc = [actor.followers_url] cc = maybe_add_group_members(cc, actor) {to, cc} end def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do to = [actor.followers_url | mentions] cc = [@ap_public] to = maybe_add_group_members(to, actor) {to, cc} end def get_to_and_cc(%Actor{} = actor, mentions, :private) do {to, cc} = get_to_and_cc(actor, mentions, :direct) to = maybe_add_group_members(to, actor) {to, cc} end def get_to_and_cc(_actor, mentions, :direct) do {mentions, []} end def get_to_and_cc(_actor, mentions, {:list, _}) do {mentions, []} end @spec maybe_add_group_members(list(String.t()), Actor.t()) :: list(String.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 @spec maybe_add_followers(list(String.t()), Actor.t()) :: list(String.t()) defp maybe_add_followers(collection, %Actor{type: :Group, followers_url: followers_url}) do [followers_url | collection] end defp maybe_add_followers(collection, %Actor{type: _}), do: collection 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_organizers(%Event{} = event, to, cc) do event = Repo.preload(event, [:organizer_actor, :attributed_to]) case event do %Event{ attributed_to: %Actor{members_url: members_url, followers_url: followers_url}, organizer_actor: %Actor{url: organizer_actor_url} } -> {to ++ [organizer_actor_url, members_url], cc ++ [followers_url]} %Event{organizer_actor: %Actor{url: organizer_actor_url}} -> {to ++ [organizer_actor_url], cc} end end defp add_event_organizers(_, to, cc), do: {to, cc} defp add_comment_author(%Comment{} = comment) do case Repo.preload(comment, [:actor]) do %Comment{actor: %Actor{url: url}} -> url _err -> nil end end defp add_comment_author(_), do: nil defp add_comments_authors(comments) do authors = comments |> Enum.map(&add_comment_author/1) |> Enum.filter(& &1) authors end @spec add_shares_actors_followers(String.t()) :: list(String.t()) defp add_shares_actors_followers(uri) do uri |> Share.get_actors_by_share_uri() |> Enum.map(& &1.url) |> Enum.uniq() end defp add_actors_that_had_our_content(actor_id) do actor_id |> Share.get_actors_by_owner_actor_id() |> Enum.map(& &1.url) |> Enum.uniq() end defp add_event_contacts(%Event{contacts: contacts}) when is_list(contacts) do contacts |> Enum.map(& &1.url) |> Enum.uniq() end defp add_event_contacts(_), do: [] defp process_mention({_, mentioned_actor}), do: mentioned_actor.url defp process_mention(%{actor_id: actor_id}) do with %Actor{url: url} <- Actors.get_actor(actor_id) do url end end @spec extract_actors_from_mentions(list(), Actor.t(), atom()) :: {list(), list()} defp extract_actors_from_mentions(mentions, actor, visibility) do get_to_and_cc(actor, Enum.map(mentions, &process_mention/1), visibility) end @spec extract_actors_from_event(Event.t()) :: %{ String.t() => list(String.t()) } defp extract_actors_from_event(%Event{} = event) do {to, cc} = extract_actors_from_mentions( event.mentions, group_or_organizer_event(event), event.visibility ) {to, cc} = {to, Enum.uniq( cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url) ++ add_event_contacts(event) )} %{"to" => to, "cc" => cc} 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