defmodule Mobilizon.Federation.ActivityPub.Audience do @moduledoc """ Tools for calculating content audience """ alias Mobilizon.Actors 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" @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 """ @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} def get_to_and_cc(%Actor{} = actor, mentions, :public) do to = [@ap_public | mentions] cc = [actor.followers_url] {to, cc} end @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do to = [actor.followers_url | mentions] cc = [@ap_public] {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} end @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} def get_to_and_cc(_actor, mentions, :direct) do {mentions, []} end def get_to_and_cc(_actor, mentions, {:list, _}) do {mentions, []} end 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_comment_author(%Comment{} = comment) do case Repo.preload(comment, [:actor]) do %Comment{actor: %Actor{url: url}} -> url _err -> nil end end 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(&Actors.list_followers_actors_for_actor/1) |> List.flatten() |> 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(&Actors.list_followers_actors_for_actor/1) |> List.flatten() |> Enum.map(& &1.url) |> Enum.uniq() end 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 with mentioned_actors <- Enum.map(mentions, &process_mention/1), addressed_actors <- get_addressed_actors(mentioned_actors, nil) do get_to_and_cc(actor, addressed_actors, visibility) end end defp extract_actors_from_event(%Event{} = event) do with {to, cc} <- extract_actors_from_mentions(event.mentions, event.organizer_actor, event.visibility), {to, cc} <- {to, Enum.uniq( cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url) )} do %{"to" => to, "cc" => cc} else _ -> %{"to" => [], "cc" => []} end end end