# Portions of this file are derived from Pleroma: # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social> # SPDX-License-Identifier: AGPL-3.0-only # Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/activity_pub.ex defmodule Mobilizon.Federation.ActivityPub do @moduledoc """ The ActivityPub context. """ import Mobilizon.Federation.ActivityPub.Utils alias Mobilizon.{ Actors, Config, Discussions, Events, Posts, Resources, Share, Users } alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Discussions.Comment alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Tombstone alias Mobilizon.Federation.ActivityPub.{ Activity, Audience, Federator, Fetcher, Preloader, Relay, Transmogrifier, Types, Visibility } alias Mobilizon.Federation.ActivityPub.Types.{Managable, Ownable} alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.HTTPSignatures.Signature alias Mobilizon.Federation.WebFinger alias Mobilizon.Service.Notifications.Scheduler alias Mobilizon.Storage.Page alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Email.{Admin, Group, Mailer} alias Mobilizon.Web.Email.Follow, as: FollowMailer require Logger @public_ap_adress "https://www.w3.org/ns/activitystreams#Public" @doc """ Wraps an object into an activity """ @spec create_activity(map(), boolean()) :: {:ok, Activity.t()} def create_activity(map, local \\ true) when is_map(map) do with map <- lazy_put_activity_defaults(map) do {:ok, %Activity{ data: map, local: local, actor: map["actor"], recipients: get_recipients(map) }} end end @doc """ Fetch an object from an URL, from our local database of events and comments, then eventually remote """ # TODO: Make database calls parallel @spec fetch_object_from_url(String.t(), Keyword.t()) :: {:ok, struct()} | {:error, any()} def fetch_object_from_url(url, options \\ []) do Logger.info("Fetching object from url #{url}") force_fetch = Keyword.get(options, :force, false) with {:not_http, true} <- {:not_http, String.starts_with?(url, "http")}, {:existing, nil} <- {:existing, Tombstone.find_tombstone(url)}, {:existing, nil} <- {:existing, Events.get_event_by_url(url)}, {:existing, nil} <- {:existing, Discussions.get_discussion_by_url(url)}, {:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)}, {:existing, nil} <- {:existing, Resources.get_resource_by_url(url)}, {:existing, nil} <- {:existing, Posts.get_post_by_url(url)}, {:existing, nil} <- {:existing, Actors.get_actor_by_url_2(url)}, {:existing, nil} <- {:existing, Actors.get_member_by_url(url)}, :ok <- Logger.info("Data for URL not found anywhere, going to fetch it"), {:ok, _activity, entity} <- Fetcher.fetch_and_create(url, options) do Logger.debug("Going to preload the new entity") Preloader.maybe_preload(entity) else {:existing, entity} -> Logger.debug("Entity is already existing") res = if force_fetch and not are_same_origin?(url, Endpoint.url()) do Logger.debug("Entity is external and we want a force fetch") case Fetcher.fetch_and_update(url, options) do {:ok, _activity, entity} -> {:ok, entity} {:error, "Gone"} -> {:error, "Gone", entity} {:error, "Not found"} -> {:error, "Not found", entity} end else {:ok, entity} end Logger.debug("Going to preload an existing entity") case res do {:ok, entity} -> Preloader.maybe_preload(entity) {:error, status, entity} -> {:ok, entity} = Preloader.maybe_preload(entity) {:error, status, entity} err -> err end e -> Logger.warn("Something failed while fetching url #{inspect(e)}") {:error, e} end end @doc """ Getting an actor from url, eventually creating it if we don't have it locally or if it needs an update """ @spec get_or_fetch_actor_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()} def get_or_fetch_actor_by_url(url, preload \\ false) def get_or_fetch_actor_by_url(nil, _preload), do: {:error, "Can't fetch a nil url"} def get_or_fetch_actor_by_url("https://www.w3.org/ns/activitystreams#Public", _preload) do with %Actor{url: url} <- Relay.get_actor() do get_or_fetch_actor_by_url(url) end end @spec get_or_fetch_actor_by_url(String.t(), boolean()) :: {:ok, Actor.t()} | {:error, any()} def get_or_fetch_actor_by_url(url, preload) do with {:ok, %Actor{} = cached_actor} <- Actors.get_actor_by_url(url, preload), false <- Actors.needs_update?(cached_actor) do {:ok, cached_actor} else _ -> # For tests, see https://github.com/jjh42/mock#not-supported---mocking-internal-function-calls and Mobilizon.Federation.ActivityPubTest case __MODULE__.make_actor_from_url(url, preload) do {:ok, %Actor{} = actor} -> {:ok, actor} err -> Logger.warn("Could not fetch by AP id") Logger.debug(inspect(err)) {:error, "Could not fetch by AP id"} end end end @doc """ Create an activity of type `Create` * Creates the object, which returns AS data * Wraps ActivityStreams data into a `Create` activity * Creates an `Mobilizon.Federation.ActivityPub.Activity` from this * Federates (asynchronously) the activity * Returns the activity """ @spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any() def create(type, args, local \\ false, additional \\ %{}) do Logger.debug("creating an activity") Logger.debug(inspect(args)) with {:tombstone, nil} <- {:tombstone, check_for_tombstones(args)}, {:ok, entity, create_data} <- (case type do :event -> Types.Events.create(args, additional) :comment -> Types.Comments.create(args, additional) :discussion -> Types.Discussions.create(args, additional) :actor -> Types.Actors.create(args, additional) :todo_list -> Types.TodoLists.create(args, additional) :todo -> Types.Todos.create(args, additional) :resource -> Types.Resources.create(args, additional) :post -> Types.Posts.create(args, additional) end), {:ok, activity} <- create_activity(create_data, local), :ok <- maybe_federate(activity), :ok <- maybe_relay_if_group_activity(activity) do {:ok, activity, entity} else err -> Logger.error("Something went wrong while creating an activity") Logger.debug(inspect(err)) err end end @doc """ Create an activity of type `Update` * Updates the object, which returns AS data * Wraps ActivityStreams data into a `Update` activity * Creates an `Mobilizon.Federation.ActivityPub.Activity` from this * Federates (asynchronously) the activity * Returns the activity """ @spec update(struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any() def update(old_entity, args, local \\ false, additional \\ %{}) do Logger.debug("updating an activity") Logger.debug(inspect(args)) with {:ok, entity, update_data} <- Managable.update(old_entity, args, additional), {:ok, activity} <- create_activity(update_data, local), :ok <- maybe_federate(activity), :ok <- maybe_relay_if_group_activity(activity) do {:ok, activity, entity} else err -> Logger.error("Something went wrong while creating an activity") Logger.debug(inspect(err)) err end end def accept(type, entity, local \\ true, additional \\ %{}) do Logger.debug("We're accepting something") {:ok, entity, update_data} = case type do :join -> accept_join(entity, additional) :follow -> accept_follow(entity, additional) :invite -> accept_invite(entity, additional) end with {:ok, activity} <- create_activity(update_data, local), :ok <- maybe_federate(activity), :ok <- maybe_relay_if_group_activity(activity) do {:ok, activity, entity} else err -> Logger.error("Something went wrong while creating an activity") Logger.debug(inspect(err)) err end end def reject(type, entity, local \\ true, additional \\ %{}) do {:ok, entity, update_data} = case type do :join -> reject_join(entity, additional) :follow -> reject_follow(entity, additional) :invite -> reject_invite(entity, additional) end with {:ok, activity} <- create_activity(update_data, local), :ok <- maybe_federate(activity), :ok <- maybe_relay_if_group_activity(activity) do {:ok, activity, entity} else err -> Logger.error("Something went wrong while creating an activity") Logger.debug(inspect(err)) err end end def announce( %Actor{} = actor, object, activity_id \\ nil, local \\ true, public \\ true ) do with {:ok, %Actor{id: object_owner_actor_id}} <- get_or_fetch_actor_by_url(object["actor"]), {:ok, %Share{} = _share} <- Share.create(object["id"], actor.id, object_owner_actor_id), announce_data <- make_announce_data(actor, object, activity_id, public), {:ok, activity} <- create_activity(announce_data, local), :ok <- maybe_federate(activity) do {:ok, activity, object} else error -> {:error, error} end end def unannounce( %Actor{} = actor, object, activity_id \\ nil, cancelled_activity_id \\ nil, local \\ true ) do with announce_activity <- make_announce_data(actor, object, cancelled_activity_id), unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id), {:ok, unannounce_activity} <- create_activity(unannounce_data, local), :ok <- maybe_federate(unannounce_activity) do {:ok, unannounce_activity, object} else _e -> {:ok, object} end end @doc """ Make an actor follow another """ def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do with {:different_actors, true} <- {:different_actors, followed.id != follower.id}, {:ok, %Follower{} = follower} <- Actors.follow(followed, follower, activity_id, false), :ok <- FollowMailer.send_notification_to_admins(follower), follower_as_data <- Convertible.model_to_as(follower), {:ok, activity} <- create_activity(follower_as_data, local), :ok <- maybe_federate(activity) do {:ok, activity, follower} else {:error, err, msg} when err in [:already_following, :suspended] -> {:error, msg} {:different_actors, _} -> {:error, "Can't follow yourself"} end end @doc """ Make an actor unfollow another """ @spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any() def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower), # We recreate the follow activity follow_as_data <- Convertible.model_to_as(%{follow | actor: follower, target_actor: followed}), {:ok, follow_activity} <- create_activity(follow_as_data, local), activity_unfollow_id <- activity_id || "#{Endpoint.url()}/unfollow/#{follow_id}/activity", unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id), {:ok, activity} <- create_activity(unfollow_data, local), :ok <- maybe_federate(activity) do {:ok, activity, follow} else err -> Logger.debug("Error while unfollowing an actor #{inspect(err)}") err end end def delete(object, actor, local \\ true, additional \\ %{}) do with {:ok, activity_data, actor, object} <- Managable.delete(object, actor, local, additional), group <- Ownable.group_actor(object), :ok <- check_for_actor_key_rotation(actor), {:ok, activity} <- create_activity(activity_data, local), :ok <- maybe_federate(activity), :ok <- maybe_relay_if_group_activity(activity, group) do {:ok, activity, object} end end def join(%Event{} = event, %Actor{} = actor, local \\ true, additional \\ %{}) do with {:ok, activity_data, participant} <- Types.Events.join(event, actor, local, additional), {:ok, activity} <- create_activity(activity_data, local), :ok <- maybe_federate(activity) do {:ok, activity, participant} else {:maximum_attendee_capacity, err} -> {:maximum_attendee_capacity, err} {:accept, accept} -> accept end end def join_group( %{parent_id: _parent_id, actor_id: _actor_id, role: _role} = args, local \\ true, additional \\ %{} ) do with {:ok, %Member{} = member} <- Mobilizon.Actors.create_member(args), activity_data when is_map(activity_data) <- Convertible.model_to_as(member), {:ok, activity} <- create_activity(Map.merge(activity_data, additional), local), :ok <- maybe_federate(activity) do {:ok, activity, member} end end def leave(object, actor, local \\ true, additional \\ %{}) @doc """ Leave an event """ def leave( %Event{id: event_id, url: event_url} = _event, %Actor{id: actor_id, url: actor_url} = _actor, local, additional ) do with {:only_organizer, false} <- {:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)}, {:ok, %Participant{} = participant} <- Mobilizon.Events.get_participant( event_id, actor_id, Map.get(additional, :metadata, %{}) ), {:ok, %Participant{} = participant} <- Events.delete_participant(participant), leave_data <- %{ "type" => "Leave", # If it's an exclusion it should be something else "actor" => actor_url, "object" => event_url, "id" => "#{Endpoint.url()}/leave/event/#{participant.id}" }, audience <- Audience.calculate_to_and_cc_from_mentions(participant), {:ok, activity} <- create_activity(Map.merge(leave_data, audience), local), :ok <- maybe_federate(activity) do {:ok, activity, participant} end end @doc """ Leave a group """ def leave( %Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url}, %Actor{id: actor_id, url: actor_url}, local, additional ) do with {:member, {:ok, %Member{id: member_id} = member}} <- {:member, Actors.get_member(actor_id, group_id)}, {:is_not_only_admin, true} <- {:is_not_only_admin, Map.get(additional, :force_member_removal, false) || !Actors.is_only_administrator?(member_id, group_id)}, {:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)}, leave_data <- %{ "to" => [group_members_url], "cc" => [group_url], "attributedTo" => group_url, "type" => "Leave", "actor" => actor_url, "object" => group_url }, {:ok, activity} <- create_activity(leave_data, local), :ok <- maybe_federate(activity), :ok <- maybe_relay_if_group_activity(activity) do {:ok, activity, member} end end def remove( %Member{} = member, %Actor{type: :Group, url: group_url, members_url: group_members_url}, %Actor{url: moderator_url}, local, _additional \\ %{} ) do with {:ok, %Member{id: member_id}} <- Actors.update_member(member, %{role: :rejected}), %Member{} = member <- Actors.get_member(member_id), :ok <- Group.send_notification_to_removed_member(member), remove_data <- %{ "to" => [group_members_url], "type" => "Remove", "actor" => moderator_url, "object" => member.actor.url, "origin" => group_url }, {:ok, activity} <- create_activity(remove_data, local), :ok <- maybe_federate(activity), :ok <- maybe_relay_if_group_activity(activity) do {:ok, activity, member} end end @spec invite(Actor.t(), Actor.t(), Actor.t(), boolean, map()) :: {:ok, map(), Member.t()} | {:error, :member_not_found} def invite( %Actor{url: group_url, id: group_id, members_url: members_url} = group, %Actor{url: actor_url, id: actor_id} = actor, %Actor{url: target_actor_url, id: target_actor_id} = _target_actor, local \\ true, additional \\ %{} ) do Logger.debug("Handling #{actor_url} invite to #{group_url} sent to #{target_actor_url}") with {:is_able_to_invite, true} <- {:is_able_to_invite, is_able_to_invite(actor, group)}, {:ok, %Member{url: member_url} = member} <- Actors.create_member(%{ parent_id: group_id, actor_id: target_actor_id, role: :invited, invited_by_id: actor_id, url: Map.get(additional, :url) }), invite_data <- %{ "type" => "Invite", "attributedTo" => group_url, "actor" => actor_url, "object" => group_url, "target" => target_actor_url, "id" => member_url }, {:ok, activity} <- create_activity( invite_data |> Map.merge(%{"to" => [target_actor_url, members_url], "cc" => [group_url]}) |> Map.merge(additional), local ), :ok <- maybe_federate(activity), :ok <- maybe_relay_if_group_activity(activity) do {:ok, activity, member} end end defp is_able_to_invite(%Actor{domain: actor_domain, id: actor_id}, %Actor{ domain: group_domain, id: group_id }) do # If the actor comes from the same domain we trust it if actor_domain == group_domain do true else # If local group, we'll send the invite with {:ok, %Member{} = admin_member} <- Actors.get_member(actor_id, group_id) do Member.is_administrator(admin_member) end end end def move(type, old_entity, args, local \\ false, additional \\ %{}) do Logger.debug("We're moving something") Logger.debug(inspect(args)) with {:ok, entity, update_data} <- (case type do :resource -> Types.Resources.move(old_entity, args, additional) end), {:ok, activity} <- create_activity(update_data, local), :ok <- maybe_federate(activity) do {:ok, activity, entity} else err -> Logger.error("Something went wrong while creating a Move activity") Logger.debug(inspect(err)) err end end def flag(args, local \\ false, additional \\ %{}) do with {report, report_as_data} <- Types.Reports.flag(args, local, additional), {:ok, activity} <- create_activity(report_as_data, local), :ok <- maybe_federate(activity) do Enum.each(Users.list_moderators(), fn moderator -> moderator |> Admin.report(report) |> Mailer.deliver_later() end) {:ok, activity, report} else err -> Logger.error("Something went wrong while creating an activity") Logger.debug(inspect(err)) err end end @doc """ Create an actor locally by its URL (AP ID) """ @spec make_actor_from_url(String.t(), boolean()) :: {:ok, %Actor{}} | {:error, any()} def make_actor_from_url(url, preload \\ false) do if are_same_origin?(url, Endpoint.url()) do {:error, "Can't make a local actor from URL"} else case fetch_and_prepare_actor_from_url(url) do {:ok, data} -> Actors.upsert_actor(data, preload) # Request returned 410 {:error, :actor_deleted} -> Logger.info("Actor was deleted") {:error, :actor_deleted} e -> Logger.warn("Failed to make actor from url") {:error, e} end end end @doc """ Find an actor in our local database or call WebFinger to find what's its AP ID is and then fetch it """ @spec find_or_make_actor_from_nickname(String.t(), atom() | nil) :: tuple() def find_or_make_actor_from_nickname(nickname, type \\ nil) do case Actors.get_actor_by_name(nickname, type) do %Actor{} = actor -> {:ok, actor} nil -> make_actor_from_nickname(nickname) end end @spec find_or_make_person_from_nickname(String.t()) :: tuple() def find_or_make_person_from_nickname(nick), do: find_or_make_actor_from_nickname(nick, :Person) @spec find_or_make_group_from_nickname(String.t()) :: tuple() def find_or_make_group_from_nickname(nick), do: find_or_make_actor_from_nickname(nick, :Group) @doc """ Create an actor inside our database from username, using WebFinger to find out its AP ID and then fetch it """ @spec make_actor_from_nickname(String.t()) :: {:ok, %Actor{}} | {:error, any()} def make_actor_from_nickname(nickname) do case WebFinger.finger(nickname) do {:ok, %{"url" => url}} when not is_nil(url) -> make_actor_from_url(url) _e -> {:error, "No ActivityPub URL found in WebFinger"} end end @spec is_create_activity?(Activity.t()) :: boolean defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true defp is_create_activity?(_), do: false @spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())} defp convert_members_in_recipients(recipients) do Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc -> case Actors.get_group_by_members_url(recipient) do # If the group is local just add external members %Actor{domain: domain} = group when is_nil(domain) -> {Enum.filter(recipients, fn recipient -> recipient != group.members_url end), member_actors ++ Actors.list_external_actors_members_for_group(group)} # If it's remote add the remote group actor as well %Actor{} = group -> {Enum.filter(recipients, fn recipient -> recipient != group.members_url end), member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]} _ -> acc end end) end defp convert_followers_in_recipients(recipients) do Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc -> case Actors.get_actor_by_followers_url(recipient) do %Actor{} = group -> {Enum.filter(recipients, fn recipient -> recipient != group.followers_url end), follower_actors ++ Actors.list_external_followers_for_actor(group)} _ -> acc end end) end # @spec is_announce_activity?(Activity.t()) :: boolean # defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true # defp is_announce_activity?(_), do: false @doc """ Publish an activity to all appropriated audiences inboxes """ # credo:disable-for-lines:47 @spec publish(Actor.t(), Activity.t()) :: :ok def publish(actor, %Activity{recipients: recipients} = activity) do Logger.debug("Publishing an activity") Logger.debug(inspect(activity)) public = Visibility.is_public?(activity) Logger.debug("is public ? #{public}") if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) Relay.publish(activity) end {recipients, followers} = convert_followers_in_recipients(recipients) {recipients, members} = convert_members_in_recipients(recipients) remote_inboxes = (remote_actors(recipients) ++ followers ++ members) |> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end) |> Enum.uniq() {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) json = Jason.encode!(data) Logger.debug(fn -> "Remote inboxes are : #{inspect(remote_inboxes)}" end) Enum.each(remote_inboxes, fn inbox -> Federator.enqueue(:publish_single_ap, %{ inbox: inbox, json: json, actor: actor, id: activity.data["id"] }) end) end @doc """ Publish an activity to a specific inbox """ def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do Logger.info("Federating #{id} to #{inbox}") %URI{host: host, path: path} = URI.parse(inbox) digest = Signature.build_digest(json) date = Signature.generate_date_header() # request_target = Signature.generate_request_target("POST", path) signature = Signature.sign(actor, %{ "(request-target)": "post #{path}", host: host, "content-length": byte_size(json), digest: digest, date: date }) Tesla.post( inbox, json, headers: [ {"Content-Type", "application/activity+json"}, {"signature", signature}, {"digest", digest}, {"date", date} ] ) end # Fetching a remote actor's information through its AP ID @spec fetch_and_prepare_actor_from_url(String.t()) :: {:ok, struct()} | {:error, atom()} | any() defp fetch_and_prepare_actor_from_url(url) do Logger.debug("Fetching and preparing actor from url") Logger.debug(inspect(url)) res = with {:ok, %{status: 200, body: body}} <- Tesla.get(url, headers: [{"Accept", "application/activity+json"}]), :ok <- Logger.debug("response okay, now decoding json"), {:ok, data} <- Jason.decode(body) do Logger.debug("Got activity+json response at actor's endpoint, now converting data") {:ok, Converter.Actor.as_to_model_data(data)} else # Actor is gone, probably deleted {:ok, %{status: 410}} -> Logger.info("Response HTTP 410") {:error, :actor_deleted} e -> Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}") {:error, e} end res end @doc """ Return all public activities (events & comments) for an actor """ @spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map() def fetch_public_activities_for_actor(%Actor{id: actor_id} = actor, page \\ 1, limit \\ 10) do %Actor{id: relay_actor_id} = Relay.get_actor() %Page{total: total_events, elements: events} = if actor_id == relay_actor_id do Events.list_public_local_events(page, limit) else Events.list_public_events_for_actor(actor, page, limit) end %Page{total: total_comments, elements: comments} = if actor_id == relay_actor_id do Discussions.list_local_comments(page, limit) else Discussions.list_public_comments_for_actor(actor, page, limit) end event_activities = Enum.map(events, &event_to_activity/1) comment_activities = Enum.map(comments, &comment_to_activity/1) activities = event_activities ++ comment_activities %{elements: activities, total: total_events + total_comments} end # Create an activity from an event @spec event_to_activity(%Event{}, boolean()) :: Activity.t() defp event_to_activity(%Event{} = event, local \\ true) do %Activity{ recipients: [@public_ap_adress], actor: event.organizer_actor.url, data: event |> Convertible.model_to_as() |> make_create_data(%{"to" => @public_ap_adress}), local: local } end # Create an activity from a comment @spec comment_to_activity(%Comment{}, boolean()) :: Activity.t() defp comment_to_activity(%Comment{} = comment, local \\ true) do %Activity{ recipients: [@public_ap_adress], actor: comment.actor.url, data: comment |> Convertible.model_to_as() |> make_create_data(%{"to" => @public_ap_adress}), local: local } end # Get recipients for an activity or object @spec get_recipients(map()) :: list() defp get_recipients(data) do Map.get(data, "to", []) ++ Map.get(data, "cc", []) end @spec check_for_tombstones(map()) :: Tombstone.t() | nil defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url) defp check_for_tombstones(_), do: nil @spec accept_follow(Follower.t(), map) :: {:ok, Follower.t(), Activity.t()} | any defp accept_follow(%Follower{} = follower, additional) do with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}), follower_as_data <- Convertible.model_to_as(follower), update_data <- make_accept_join_data( follower_as_data, Map.merge(additional, %{ "id" => "#{Endpoint.url()}/accept/follow/#{follower.id}", "to" => [follower.actor.url], "cc" => [], "actor" => follower.target_actor.url }) ) do {:ok, follower, update_data} else err -> Logger.error("Something went wrong while creating an update activity") Logger.debug(inspect(err)) err end end @spec accept_join(Participant.t(), map) :: {:ok, Participant.t(), Activity.t()} | any defp accept_join(%Participant{} = participant, additional) do with {:ok, %Participant{} = participant} <- Events.update_participant(participant, %{role: :participant}), Absinthe.Subscription.publish(Endpoint, participant.actor, event_person_participation_changed: participant.actor.id ), {:ok, _} <- Scheduler.trigger_notifications_for_participant(participant), participant_as_data <- Convertible.model_to_as(participant), audience <- Audience.calculate_to_and_cc_from_mentions(participant), update_data <- make_accept_join_data( participant_as_data, Map.merge(Map.merge(audience, additional), %{ "id" => "#{Endpoint.url()}/accept/join/#{participant.id}" }) ) do {:ok, participant, update_data} else err -> Logger.error("Something went wrong while creating an update activity") Logger.debug(inspect(err)) err end end @spec accept_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any defp accept_invite( %Member{invited_by_id: invited_by_id, actor_id: actor_id} = member, _additional ) do with %Actor{} = inviter <- Actors.get_actor(invited_by_id), %Actor{url: actor_url} <- Actors.get_actor(actor_id), {:ok, %Member{id: member_id} = member} <- Actors.update_member(member, %{role: :member}), accept_data <- %{ "type" => "Accept", "attributedTo" => member.parent.url, "to" => [inviter.url, member.parent.members_url], "cc" => [member.parent.url], "actor" => actor_url, "object" => Convertible.model_to_as(member), "id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}" } do {:ok, member, accept_data} end end @spec reject_join(Participant.t(), map()) :: {:ok, Participant.t(), Activity.t()} | any() defp reject_join(%Participant{} = participant, additional) do with {:ok, %Participant{} = participant} <- Events.update_participant(participant, %{role: :rejected}), Absinthe.Subscription.publish(Endpoint, participant.actor, event_person_participation_changed: participant.actor.id ), participant_as_data <- Convertible.model_to_as(participant), audience <- participant |> Audience.calculate_to_and_cc_from_mentions() |> Map.merge(additional), reject_data <- %{ "type" => "Reject", "object" => participant_as_data }, update_data <- reject_data |> Map.merge(audience) |> Map.merge(%{ "id" => "#{Endpoint.url()}/reject/join/#{participant.id}" }) do {:ok, participant, update_data} else err -> Logger.error("Something went wrong while creating an update activity") Logger.debug(inspect(err)) err end end @spec reject_follow(Follower.t(), map()) :: {:ok, Follower.t(), Activity.t()} | any() defp reject_follow(%Follower{} = follower, additional) 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), reject_data <- %{ "to" => [follower.actor.url], "type" => "Reject", "actor" => follower.target_actor.url, "object" => follower_as_data }, update_data <- audience |> Map.merge(reject_data) |> Map.merge(%{ "id" => "#{Endpoint.url()}/reject/follow/#{follower.id}" }) do {:ok, follower, update_data} else err -> Logger.error("Something went wrong while creating an update activity") Logger.debug(inspect(err)) err end end @spec reject_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any defp reject_invite( %Member{invited_by_id: invited_by_id, actor_id: actor_id} = member, _additional ) do with %Actor{} = inviter <- Actors.get_actor(invited_by_id), %Actor{url: actor_url} <- Actors.get_actor(actor_id), {:ok, %Member{url: member_url, id: member_id} = member} <- Actors.delete_member(member), accept_data <- %{ "type" => "Reject", "actor" => actor_url, "attributedTo" => member.parent.url, "to" => [inviter.url, member.parent.members_url], "cc" => [member.parent.url], "object" => member_url, "id" => "#{Endpoint.url()}/reject/invite/member/#{member_id}" } do {:ok, member, accept_data} end end end