debianize-mobilizon/lib/federation/activity_pub/audience.ex
Thomas Citharel b5672cee7e
WIP
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2023-11-14 14:24:17 +01:00

283 lines
8.1 KiB
Elixir

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.Conversations.Conversation
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
def get_audience(%Conversation{participants: participants}) do
%{"to" => Enum.map(participants, & &1.url), "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