mobilizon/lib/federation/activity_pub/transmogrifier.ex
Thomas Citharel 42f07c17e5
Disable mix docs for now
Because of https://github.com/elixir-lang/ex_doc/issues/1172

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-06-04 10:58:27 +02:00

725 lines
25 KiB
Elixir
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/transmogrifier.ex
defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Mobilizon.{Actors, Conversations, Events, Resources, Todos}
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Conversations.Comment
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Web.Email.{Group, Participation}
require Logger
def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error
def handle_incoming(%{"type" => "Flag"} = data) do
with params <- Converter.Flag.as_to_model(data) do
params = %{
reporter_id: params["reporter"].id,
reported_id: params["reported"].id,
comments_ids: params["comments"] |> Enum.map(& &1.id),
content: params["content"] || "",
additional: %{
"cc" => [params["reported"].url]
},
event_id: if(is_nil(params["event"]), do: nil, else: params["event"].id || nil),
local: false
}
ActivityPub.flag(params, false)
end
end
@doc """
Handles a `Create` activity for `Note` (comments) objects
The following actions are performed
* Fetch the author of the activity
* Convert the ActivityStream data to the comment model format (it also finds and inserts tags)
* Get (by it's URL) or create the comment with this data
* Insert eventual mentions in the database
* Convert the comment back in ActivityStreams data
* Wrap this data back into a `Create` activity
* Return the activity and the comment object
"""
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object}) do
Logger.info("Handle incoming to create notes")
with object_data <-
object |> Converter.Comment.as_to_model_data(),
{:existing_comment, {:error, :comment_not_found}} <-
{:existing_comment, Conversations.get_comment_from_url_with_preload(object_data.url)},
{:ok, %Activity{} = activity, %Comment{} = comment} <-
ActivityPub.create(:comment, object_data, false) do
{:ok, activity, comment}
else
{:existing_comment, {:ok, %Comment{} = comment}} ->
{:ok, nil, comment}
end
end
@doc """
Handles a `Create` activity for `Event` objects
The following actions are performed
* Fetch the author of the activity
* Convert the ActivityStream data to the event model format (it also finds and inserts tags)
* Get (by it's URL) or create the event with this data
* Insert eventual mentions in the database
* Convert the event back in ActivityStreams data
* Wrap this data back into a `Create` activity
* Return the activity and the event object
"""
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object}) do
Logger.info("Handle incoming to create event")
with object_data <-
object |> Converter.Event.as_to_model_data(),
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Event{} = event} <-
ActivityPub.create(:event, object_data, false) do
{:ok, activity, event}
else
{:existing_event, %Event{} = event} -> {:ok, nil, event}
end
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
) do
with {:ok, %Actor{} = followed} <- ActivityPub.get_or_fetch_actor_by_url(followed, true),
{:ok, %Actor{} = follower} <- ActivityPub.get_or_fetch_actor_by_url(follower),
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
{:ok, activity, object}
else
e ->
Logger.warn("Unable to handle Follow activity #{inspect(e)}")
:error
end
end
def handle_incoming(%{
"type" => "Create",
"object" => %{"type" => "TodoList", "id" => object_url} = object,
"actor" => actor_url
}) do
Logger.info("Handle incoming to create a todo list")
with {:existing_todo_list, nil} <-
{:existing_todo_list, Todos.get_todo_list_by_url(object_url)},
{:ok, %Actor{url: actor_url}} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
object_data when is_map(object_data) <-
object |> Converter.TodoList.as_to_model_data(),
{:ok, %Activity{} = activity, %TodoList{} = todo_list} <-
ActivityPub.create(:todo_list, object_data, false, %{"actor" => actor_url}) do
{:ok, activity, todo_list}
else
{:error, :group_not_found} -> :error
{:existing_todo_list, %TodoList{} = todo_list} -> {:ok, nil, todo_list}
end
end
def handle_incoming(%{
"type" => "Create",
"object" => %{"type" => "Todo", "id" => object_url} = object
}) do
Logger.info("Handle incoming to create a todo")
with {:existing_todo, nil} <-
{:existing_todo, Todos.get_todo_by_url(object_url)},
object_data <-
object |> Converter.Todo.as_to_model_data(),
{:ok, %Activity{} = activity, %Todo{} = todo} <-
ActivityPub.create(:todo, object_data, false) do
{:ok, activity, todo}
else
{:existing_todo, %Todo{} = todo} -> {:ok, nil, todo}
end
end
def handle_incoming(
%{
"type" => activity_type,
"object" => %{"type" => object_type, "id" => object_url} = object,
"to" => to
} = data
)
when activity_type in ["Create", "Add"] and
object_type in ["Document", "ResourceCollection"] do
Logger.info("Handle incoming to create a resource")
Logger.debug(inspect(data))
group_url = hd(to)
with {:existing_resource, nil} <-
{:existing_resource, Resources.get_resource_by_url(object_url)},
object_data when is_map(object_data) <-
object |> Converter.Resource.as_to_model_data(),
{:member, true} <-
{:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)},
{:ok, %Activity{} = activity, %Resource{} = resource} <-
ActivityPub.create(:resource, object_data, false),
{:ok, %Actor{type: :Group, id: group_id} = group} <- Actors.get_actor_by_url(group_url),
announce_id <- "#{object_url}/announces/#{group_id}",
{:ok, _activity, _resource} <- ActivityPub.announce(group, object, announce_id) do
{:ok, activity, resource}
else
{:existing_resource, %Resource{} = resource} ->
{:ok, nil, resource}
{:member, false} ->
# At some point this should refresh the list of group members
# if the group is not local before simply returning an error
:error
{:error, e} ->
Logger.error(inspect(e))
:error
end
end
def handle_incoming(
%{
"type" => "Accept",
"object" => accepted_object,
"actor" => _actor,
"id" => id
} = data
) do
with actor_url <- Utils.get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
{:object_not_found, {:ok, %Activity{} = activity, object}} <-
{:object_not_found,
do_handle_incoming_accept_following(accepted_object, actor) ||
do_handle_incoming_accept_join(accepted_object, actor)} do
{:ok, activity, object}
else
{:object_not_found, nil} ->
Logger.warn(
"Unable to process Accept activity #{inspect(id)}. Object #{inspect(accepted_object)} wasn't found."
)
:error
e ->
Logger.warn(
"Unable to process Accept activity #{inspect(id)} for object #{inspect(accepted_object)} only returned #{
inspect(e)
}"
)
:error
end
end
def handle_incoming(
%{"type" => "Reject", "object" => rejected_object, "actor" => _actor, "id" => id} = data
) do
with actor_url <- Utils.get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
{:object_not_found, {:ok, activity, object}} <-
{:object_not_found,
do_handle_incoming_reject_following(rejected_object, actor) ||
do_handle_incoming_reject_join(rejected_object, actor)} do
{:ok, activity, object}
else
{:object_not_found, nil} ->
Logger.warn(
"Unable to process Reject activity #{inspect(id)}. Object #{inspect(rejected_object)} wasn't found."
)
:error
e ->
Logger.warn(
"Unable to process Reject activity #{inspect(id)} for object #{inspect(rejected_object)} only returned #{
inspect(e)
}"
)
:error
end
end
def handle_incoming(
%{"type" => "Announce", "object" => object, "actor" => _actor, "id" => _id} = data
) do
with actor <- Utils.get_actor(data),
# TODO: Is the following line useful?
{:ok, %Actor{id: actor_id} = _actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
:ok <- Logger.debug("Fetching contained object"),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object),
:ok <- Logger.debug("Handling contained object"),
create_data <- Utils.make_create_data(object),
:ok <- Logger.debug(inspect(object)),
{:ok, _activity, entity} <- handle_incoming(create_data),
:ok <- Logger.debug("Finished processing contained object"),
{:ok, activity} <- ActivityPub.create_activity(data, false),
{:ok, %Actor{id: object_owner_actor_id}} <- Actors.get_actor_by_url(object["actor"]),
{:ok, %Mobilizon.Share{} = _share} <-
Mobilizon.Share.create(object["id"], actor_id, object_owner_actor_id) do
{:ok, activity, entity}
else
e ->
Logger.debug(inspect(e))
:error
end
end
def handle_incoming(%{
"type" => "Update",
"object" => %{"type" => object_type} = object,
"actor" => _actor_id
})
when object_type in ["Person", "Group", "Application", "Service", "Organization"] do
with {:ok, %Actor{} = old_actor} <- Actors.get_actor_by_url(object["id"]),
object_data <-
object |> Converter.Actor.as_to_model_data(),
{:ok, %Activity{} = activity, %Actor{} = new_actor} <-
ActivityPub.update(:actor, old_actor, object_data, false) do
{:ok, activity, new_actor}
else
e ->
Logger.debug(inspect(e))
:error
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => _actor} =
update_data
) do
with actor <- Utils.get_actor(update_data),
{:ok, %Actor{url: actor_url}} <- Actors.get_actor_by_url(actor),
{:ok, %Event{} = old_event} <-
object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
object_data <- Converter.Event.as_to_model_data(object),
{:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)},
{:ok, %Activity{} = activity, %Event{} = new_event} <-
ActivityPub.update(:event, old_event, object_data, false) do
{:ok, activity, new_event}
else
_e ->
:error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{
"type" => "Announce",
"object" => object_id,
"id" => cancelled_activity_id
},
"actor" => _actor,
"id" => id
} = data
) do
with actor <- Utils.get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
{:ok, activity, object} <-
ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
{:ok, activity, object}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Follow", "object" => followed},
"actor" => follower,
"id" => id
} = _data
) do
with {:ok, %Actor{domain: nil} = followed} <- Actors.get_actor_by_url(followed),
{:ok, %Actor{} = follower} <- Actors.get_actor_by_url(follower),
{:ok, activity, object} <- ActivityPub.unfollow(follower, followed, id, false) do
{:ok, activity, object}
else
e ->
Logger.debug(inspect(e))
:error
end
end
# TODO: We presently assume that any actor on the same origin domain as the object being
# deleted has the rights to delete that object. A better way to validate whether or not
# the object should be deleted is to refetch the object URI, which should return either
# an error or a tombstone. This would allow us to verify that a deletion actually took
# place.
def handle_incoming(
%{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data
) do
with actor <- Utils.get_actor(data),
{:ok, %Actor{url: actor_url}} <- Actors.get_actor_by_url(actor),
object_id <- Utils.get_url(object),
{:origin_check, true} <-
{:origin_check, Utils.origin_check_from_id?(actor_url, object_id)},
{:ok, object} <- ActivityPub.fetch_object_from_url(object_id),
{:ok, activity, object} <- ActivityPub.delete(object, false) do
{:ok, activity, object}
else
{:origin_check, false} ->
Logger.warn("Object origin check failed")
:error
e ->
Logger.debug(inspect(e))
:error
end
end
def handle_incoming(
%{
"type" => "Join",
"object" => object,
"actor" => _actor,
"id" => id,
"participationMessage" => note
} = data
) do
with actor <- Utils.get_actor(data),
{:ok, %Actor{url: _actor_url} = actor} <- Actors.get_actor_by_url(actor),
object <- Utils.get_url(object),
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
{:ok, activity, object} <-
ActivityPub.join(object, actor, false, %{url: id, metadata: %{message: note}}) do
{:ok, activity, object}
else
e ->
Logger.debug(inspect(e))
:error
end
end
def handle_incoming(
%{"type" => "Leave", "object" => object, "actor" => actor, "id" => _id} = data
) do
with actor <- Utils.get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_actor_by_url(actor),
object <- Utils.get_url(object),
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
{:ok, activity, object} <- ActivityPub.leave(object, actor, false) do
{:ok, activity, object}
else
{:only_organizer, true} ->
Logger.warn(
"Actor #{inspect(actor)} tried to leave event #{inspect(object)} but it was the only organizer so we didn't detach it"
)
:error
_e ->
:error
end
end
def handle_incoming(
%{
"type" => "Invite",
"object" => object,
"actor" => _actor,
"id" => id,
"target" => target
} = data
) do
with {:ok, %Actor{} = actor} <- data |> Utils.get_actor() |> Actors.get_actor_by_url(),
{:ok, object} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
{:ok, %Actor{} = target} <-
target |> Utils.get_url() |> ActivityPub.get_or_fetch_actor_by_url(),
{:ok, activity, %Member{} = member} <-
ActivityPub.invite(object, actor, target, false, %{url: id}),
:ok <- Group.send_invite_to_user(member) do
{:ok, activity, member}
end
end
#
# # TODO
# # Accept
# # Undo
#
# def handle_incoming(
# %{
# "type" => "Undo",
# "object" => %{"type" => "Like", "object" => object_id},
# "actor" => _actor,
# "id" => id
# } = data
# ) do
# with actor <- Utils.get_actor(data),
# %Actor{} = actor <- ActivityPub.get_or_fetch_actor_by_url(actor),
# {:ok, object} <- fetch_obj_helper(object_id) || fetch_obj_helper(object_id),
# {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
# {:ok, activity}
# else
# _e -> :error
# end
# end
def handle_incoming(object) do
Logger.info("Handing something not supported")
Logger.debug(inspect(object))
{:error, :not_supported}
end
@doc """
Handle incoming `Accept` activities wrapping a `Follow` activity
"""
def do_handle_incoming_accept_following(follow_object, %Actor{} = actor) do
with {:follow, {:ok, %Follower{approved: false, target_actor: followed} = follow}} <-
{:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
ActivityPub.accept(
:follow,
follow,
false
) do
{:ok, activity, follow}
else
{:follow, _} ->
Logger.debug(
"Tried to handle an Accept activity but it's not containing a Follow activity"
)
nil
{:same_actor} ->
{:error, "Actor who accepted the follow wasn't the target. Quite odd."}
{:ok, %Follower{approved: true} = _follow} ->
{:error, "Follow already accepted"}
end
end
@doc """
Handle incoming `Reject` activities wrapping a `Follow` activity
"""
def do_handle_incoming_reject_following(follow_object, %Actor{} = actor) do
with {:follow, {:ok, %Follower{target_actor: followed} = follow}} <-
{:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, activity, _} <-
ActivityPub.reject(:follow, follow) do
{:ok, activity, follow}
else
{:follow, _err} ->
Logger.debug(
"Tried to handle a Reject activity but it's not containing a Follow activity"
)
nil
{:same_actor} ->
{:error, "Actor who rejected the follow wasn't the target. Quite odd."}
{:ok, %Follower{approved: true} = _follow} ->
{:error, "Follow already accepted"}
end
end
# Handle incoming `Accept` activities wrapping a `Join` activity on an event
defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
case get_participant(join_object) do
{:ok, participant} ->
do_handle_incoming_accept_join_event(participant, actor_accepting)
{:error, _err} ->
case get_member(join_object) do
{:ok, member} ->
do_handle_incoming_accept_join_group(member, actor_accepting)
{:error, _err} ->
nil
end
end
end
defp do_handle_incoming_accept_join_event(%Participant{role: :participant}, _actor) do
Logger.debug(
"Tried to handle an Accept activity on a Join activity with a event object but the participant is already validated"
)
nil
end
defp do_handle_incoming_accept_join_event(
%Participant{role: role, event: event} = participant,
%Actor{} = actor_accepting
)
when role in [:not_approved, :rejected] do
# TODO: The actor that accepts the Join activity may another one that the event organizer ?
# Or maybe for groups it's the group that sends the Accept activity
with {:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
{:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <-
ActivityPub.accept(
:join,
participant,
false
),
:ok <-
Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant}
else
{:same_actor} ->
{:error, "Actor who accepted the join wasn't the event organizer. Quite odd."}
{:ok, %Participant{role: :participant} = _follow} ->
{:error, "Participant"}
end
end
defp do_handle_incoming_accept_join_group(%Member{role: :member}, _actor) do
Logger.debug(
"Tried to handle an Accept activity on a Join activity with a group object but the member is already validated"
)
nil
end
defp do_handle_incoming_accept_join_group(
%Member{role: role, parent: _group} = member,
%Actor{} = _actor_accepting
)
when role in [:not_approved, :rejected, :invited] do
# TODO: The actor that accepts the Join activity may another one that the event organizer ?
# Or maybe for groups it's the group that sends the Accept activity
with {:ok, %Activity{} = activity, %Member{role: :member} = member} <-
ActivityPub.accept(
:invite,
member,
false
) do
{:ok, activity, member}
end
end
# Handle incoming `Reject` activities wrapping a `Join` activity on an event
defp do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do
with {:join_event, {:ok, %Participant{event: event, role: role} = participant}}
when role != :rejected <-
{:join_event, get_participant(join_object)},
# TODO: The actor that accepts the Join activity may another one that the event organizer ?
# Or maybe for groups it's the group that sends the Accept activity
{:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
{:ok, activity, participant} <-
ActivityPub.reject(:join, participant, false),
:ok <- Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant}
else
{:join_event, {:ok, %Participant{role: :rejected}}} ->
Logger.warn(
"Tried to handle an Reject activity on a Join activity with a event object but the participant is already rejected"
)
nil
{:join_event, _err} ->
Logger.debug(
"Tried to handle an Reject activity but it's not containing a Join activity on a event"
)
nil
{:same_actor} ->
{:error, "Actor who rejected the join wasn't the event organizer. Quite odd."}
{:ok, %Participant{role: :participant} = _follow} ->
{:error, "Participant"}
end
end
defp get_follow(follow_object) do
with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object),
{:not_found, %Follower{} = follow} <-
{:not_found, Actors.get_follower_by_url(follow_object_id)} do
{:ok, follow}
else
{:not_found, _err} ->
{:error, "Follow URL not found"}
_ ->
{:error, "ActivityPub ID not found in Accept Follow object"}
end
end
defp get_participant(join_object) do
with join_object_id when not is_nil(join_object_id) <- Utils.get_url(join_object),
{:not_found, %Participant{} = participant} <-
{:not_found, Events.get_participant_by_url(join_object_id)} do
{:ok, participant}
else
{:not_found, _err} ->
{:error, "Participant URL not found"}
_ ->
{:error, "ActivityPub ID not found in Accept Join object"}
end
end
@spec get_member(String.t() | map()) :: {:ok, Member.t()} | {:error, String.t()}
defp get_member(member_object) do
with member_object_id when not is_nil(member_object_id) <- Utils.get_url(member_object),
%Member{} = member <-
Actors.get_member_by_url(member_object_id) do
{:ok, member}
else
{:error, :member_not_found} ->
{:error, "Member URL not found"}
_ ->
{:error, "ActivityPub ID not found in Accept Join object"}
end
end
def prepare_outgoing(%{"type" => _type} = data) do
data =
data
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
@spec fetch_obj_helper(map() | String.t()) :: Event.t() | Comment.t() | Actor.t() | any()
def fetch_obj_helper(object) do
Logger.debug("fetch_obj_helper")
Logger.debug("Fetching object #{inspect(object)}")
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
{:ok, object} ->
{:ok, object}
err ->
Logger.warn("Error while fetching #{inspect(object)}")
{:error, err}
end
end
def fetch_obj_helper_as_activity_streams(object) do
Logger.debug("fetch_obj_helper_as_activity_streams")
with {:ok, object} <- fetch_obj_helper(object) do
{:ok, Convertible.model_to_as(object)}
end
end
end