Introduce group posts

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-07-09 17:24:28 +02:00
parent bec1c69d4b
commit 9c9f1385fb
249 changed files with 11886 additions and 5023 deletions

View File

@@ -13,41 +13,38 @@ defmodule Mobilizon.Federation.ActivityPub do
alias Mobilizon.{
Actors,
Config,
Conversations,
Discussions,
Events,
Reports,
Resources,
Share,
Todos,
Users
}
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Conversations.Comment
alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Reports.Report
alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList}
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.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.HTTPSignatures.Signature
alias Mobilizon.Federation.WebFinger
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
alias Mobilizon.Service.Formatter.HTML
alias Mobilizon.Service.Notifications.Scheduler
alias Mobilizon.Service.RichMedia.Parser
alias Mobilizon.Storage.Page
alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Email.{Admin, Mailer}
@@ -74,75 +71,44 @@ defmodule Mobilizon.Federation.ActivityPub do
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()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()}
def fetch_object_from_url(url) do
@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_event, nil} <- {:existing_event, Events.get_event_by_url(url)},
{:existing_comment, nil} <- {:existing_comment, Conversations.get_comment_from_url(url)},
{:existing_resource, nil} <- {:existing_resource, Resources.get_resource_by_url(url)},
{:existing_actor, {:error, :actor_not_found}} <-
{:existing_actor, Actors.get_actor_by_url(url)},
date <- Signature.generate_date_header(),
headers <-
[{:Accept, "application/activity+json"}]
|> maybe_date_fetch(date)
|> sign_fetch_relay(url, date),
{:ok, %HTTPoison.Response{body: body, status_code: code}} when code in 200..299 <-
HTTPoison.get(
url,
headers,
follow_redirect: true,
timeout: 10_000,
recv_timeout: 20_000,
ssl: [{:versions, [:"tlsv1.2"]}]
),
{:ok, data} <- Jason.decode(body),
{:origin_check, true} <- {:origin_check, origin_check?(url, data)},
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["attributedTo"],
"object" => data
},
{:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
case data["type"] do
"Event" ->
{:ok, Events.get_public_event_by_url_with_preload!(object_url)}
"Note" ->
{:ok, Conversations.get_comment_from_url_with_preload!(object_url)}
"Document" ->
{:ok, Resources.get_resource_by_url_with_preloads(object_url)}
"ResourceCollection" ->
{:ok, Resources.get_resource_by_url_with_preloads(object_url)}
"Actor" ->
{:ok, Actors.get_actor_by_url!(object_url, true)}
other ->
{:error, other}
end
{: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, Actors.get_actor_by_url_2(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_event, %Event{url: event_url}} ->
{:ok, Events.get_public_event_by_url_with_preload!(event_url)}
{:existing, entity} ->
Logger.debug("Entity is already existing")
{:existing_comment, %Comment{url: comment_url}} ->
{:ok, Conversations.get_comment_from_url_with_preload!(comment_url)}
entity =
if force_fetch and not compare_origins?(url, Endpoint.url()) do
Logger.debug("Entity is external and we want a force fetch")
{:existing_resource, %Resource{url: resource_url}} ->
{:ok, Resources.get_resource_by_url_with_preloads(resource_url)}
with {:ok, _activity, entity} <- Fetcher.fetch_and_update(url, options) do
entity
end
else
entity
end
{:existing_actor, {:ok, %Actor{url: actor_url}}} ->
{:ok, Actors.get_actor_by_url!(actor_url, true)}
Logger.debug("Going to preload an existing entity")
{:origin_check, false} ->
Logger.warn("Object origin check failed")
{:error, "Object origin check failed"}
Preloader.maybe_preload(entity)
e ->
Logger.warn("Something failed while fetching url #{inspect(e)}")
@@ -201,15 +167,18 @@ defmodule Mobilizon.Federation.ActivityPub do
with {:tombstone, nil} <- {:tombstone, check_for_tombstones(args)},
{:ok, entity, create_data} <-
(case type do
:event -> create_event(args, additional)
:comment -> create_comment(args, additional)
:group -> create_group(args, additional)
:todo_list -> create_todo_list(args, additional)
:todo -> create_todo(args, additional)
:resource -> create_resource(args, additional)
: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) do
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, entity}
else
err ->
@@ -227,21 +196,15 @@ defmodule Mobilizon.Federation.ActivityPub do
* Federates (asynchronously) the activity
* Returns the activity
"""
@spec update(atom(), struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
def update(type, old_entity, args, local \\ false, additional \\ %{}) do
@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} <-
(case type do
:event -> update_event(old_entity, args, additional)
:comment -> update_comment(old_entity, args, additional)
:actor -> update_actor(old_entity, args, additional)
:todo -> update_todo(old_entity, args, additional)
:resource -> update_resource(old_entity, args, additional)
end),
with {:ok, entity, update_data} <- Managable.update(old_entity, args, additional),
{:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, entity}
else
err ->
@@ -366,182 +329,48 @@ defmodule Mobilizon.Federation.ActivityPub do
end
end
def delete(object, local \\ true)
@spec delete(Event.t(), boolean) :: {:ok, Activity.t(), Event.t()}
def delete(%Event{url: url, organizer_actor: actor} = event, local) do
data = %{
"type" => "Delete",
"actor" => actor.url,
"object" => url,
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"],
"id" => url <> "/delete"
}
with audience <-
Audience.calculate_to_and_cc_from_mentions(event),
{:ok, %Event{} = event} <- Events.delete_event(event),
{:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"),
{:ok, %Tombstone{} = _tombstone} <-
Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}),
Share.delete_all_by_uri(event.url),
def delete(object, actor, local \\ true) do
with {:ok, activity_data, actor, object} <-
Managable.delete(object, actor, local),
group <- Ownable.group_actor(object),
:ok <- check_for_actor_key_rotation(actor),
{:ok, activity} <- create_activity(Map.merge(data, audience), local),
:ok <- maybe_federate(activity) do
{:ok, activity, event}
{: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
@spec delete(Comment.t(), boolean) :: {:ok, Activity.t(), Comment.t()}
def delete(%Comment{url: url, actor: actor} = comment, local) do
data = %{
"type" => "Delete",
"actor" => actor.url,
"object" => url,
"id" => url <> "/delete",
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
}
with audience <-
Audience.calculate_to_and_cc_from_mentions(comment),
{:ok, %Comment{} = comment} <- Conversations.delete_comment(comment),
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
{:ok, %Tombstone{} = _tombstone} <-
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}),
Share.delete_all_by_uri(comment.url),
:ok <- check_for_actor_key_rotation(actor),
{:ok, activity} <- create_activity(Map.merge(data, audience), local),
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, comment}
end
end
def delete(%Actor{url: url} = actor, local) do
data = %{
"type" => "Delete",
"actor" => url,
"object" => url,
"id" => url <> "/delete",
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
}
# We completely delete the actor if activity is remote
with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor, reserve_username: local),
{:ok, activity} <- create_activity(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, actor}
end
end
def delete(
%Resource{url: url, actor: %Actor{url: actor_url}} = resource,
local
) do
Logger.debug("Building Delete Resource activity")
data = %{
"actor" => actor_url,
"type" => "Delete",
"object" => url,
"id" => url <> "/delete",
"to" => [actor_url]
}
Logger.debug(inspect(data))
with {:ok, _resource} <- Resources.delete_resource(resource),
{:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}"),
{:ok, activity} <- create_activity(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, resource}
end
end
def flag(args, local \\ false, _additional \\ %{}) do
with {:build_args, args} <- {:build_args, prepare_args_for_report(args)},
{:create_report, {:ok, %Report{} = report}} <-
{:create_report, Reports.create_report(args)},
report_as_data <- Convertible.model_to_as(report),
cc <- if(local, do: [report.reported.url], else: []),
report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}),
{: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}
{:ok, activity, participant}
else
err ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
{:maximum_attendee_capacity, err} ->
{:maximum_attendee_capacity, err}
{:accept, accept} ->
accept
end
end
def join(object, actor, local \\ true, additional \\ %{})
def join(%Event{} = event, %Actor{} = actor, local, additional) do
# TODO Refactor me for federation
with {:maximum_attendee_capacity, true} <-
{:maximum_attendee_capacity, check_attendee_capacity(event)},
role <-
additional
|> Map.get(:metadata, %{})
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)),
{:ok, %Participant{} = participant} <-
Mobilizon.Events.create_participant(%{
role: role,
event_id: event.id,
actor_id: actor.id,
url: Map.get(additional, :url),
metadata:
additional
|> Map.get(:metadata, %{})
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
def join_group(
%{parent_id: parent_id, actor_id: actor_id, role: role},
local \\ true,
additional \\ %{}
) do
with {:ok, %Member{} = member} <-
Mobilizon.Actors.create_member(%{
parent_id: parent_id,
actor_id: actor_id,
role: role
}),
join_data <- Convertible.model_to_as(participant),
audience <-
Audience.calculate_to_and_cc_from_mentions(participant),
{:ok, activity} <- create_activity(Map.merge(join_data, audience), local),
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
if event.local do
cond do
Mobilizon.Events.get_default_participant_role(event) === :participant &&
role == :participant ->
accept(
:join,
participant,
true,
%{"actor" => event.organizer_actor.url}
)
Mobilizon.Events.get_default_participant_role(event) === :not_approved &&
role == :not_approved ->
Scheduler.pending_participation_notification(event)
{:ok, activity, participant}
true ->
{:ok, activity, participant}
end
else
{:ok, activity, participant}
end
end
end
# TODO: Implement me
def join(%Actor{type: :Group} = _group, %Actor{} = _actor, _local, _additional) do
:error
end
defp check_attendee_capacity(%Event{options: options} = event) do
with maximum_attendee_capacity <-
Map.get(options, :maximum_attendee_capacity) || 0 do
maximum_attendee_capacity == 0 ||
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
{:ok, activity, member}
end
end
@@ -640,7 +469,7 @@ defmodule Mobilizon.Federation.ActivityPub do
with {:ok, entity, update_data} <-
(case type do
:resource -> move_resource(old_entity, args, additional)
:resource -> Types.Resources.move(old_entity, args, additional)
end),
{:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do
@@ -653,6 +482,25 @@ defmodule Mobilizon.Federation.ActivityPub do
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)
"""
@@ -711,9 +559,29 @@ defmodule Mobilizon.Federation.ActivityPub do
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
defp is_create_activity?(_), do: false
@spec is_announce_activity?(Activity.t()) :: boolean
defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true
defp is_announce_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
# @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
@@ -741,19 +609,11 @@ defmodule Mobilizon.Federation.ActivityPub do
{recipients, []}
end
# If we want to send to all members of the group, because this server is the one the group is on
{recipients, members} =
if is_announce_activity?(activity) and actor.type == :Group and
actor.members_url in activity.recipients and is_nil(actor.domain) do
{Enum.filter(recipients, fn recipient -> recipient != actor.members_url end),
Actors.list_external_members_for_group(actor)}
else
{recipients, []}
end
{recipients, members} = convert_members_in_recipients(recipients)
remote_inboxes =
(remote_actors(recipients) ++ followers ++ members)
|> Enum.map(fn follower -> follower.shared_inbox_url || follower.inbox_url end)
|> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end)
|> Enum.uniq()
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
@@ -791,16 +651,15 @@ defmodule Mobilizon.Federation.ActivityPub do
date: date
})
HTTPoison.post(
Tesla.post(
inbox,
json,
[
headers: [
{"Content-Type", "application/activity+json"},
{"signature", signature},
{"digest", digest},
{"date", date}
],
hackney: [pool: :default]
]
)
end
@@ -811,18 +670,15 @@ defmodule Mobilizon.Federation.ActivityPub do
Logger.debug(inspect(url))
res =
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
HTTPoison.get(url, [Accept: "application/activity+json"],
follow_redirect: true,
ssl: [{:versions, [:"tlsv1.2"]}]
),
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, %HTTPoison.Response{status_code: 410}} ->
{:ok, %{status: 410}} ->
Logger.info("Response HTTP 410")
{:error, :actor_deleted}
@@ -839,10 +695,11 @@ defmodule Mobilizon.Federation.ActivityPub do
"""
@spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map()
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 1, limit \\ 10) do
{:ok, events, total_events} = Events.list_public_events_for_actor(actor, page, limit)
%Page{total: total_events, elements: events} =
Events.list_public_events_for_actor(actor, page, limit)
{:ok, comments, total_comments} =
Conversations.list_public_comments_for_actor(actor, page, limit)
%Page{total: total_comments, elements: comments} =
Discussions.list_public_comments_for_actor(actor, page, limit)
event_activities = Enum.map(events, &event_to_activity/1)
comment_activities = Enum.map(comments, &comment_to_activity/1)
@@ -879,252 +736,10 @@ defmodule Mobilizon.Federation.ActivityPub do
Map.get(data, "to", []) ++ Map.get(data, "cc", [])
end
@spec create_event(map(), map()) :: {:ok, map()}
defp create_event(args, additional) do
with args <- prepare_args_for_event(args),
{:ok, %Event{} = event} <- Events.create_event(args),
event_as_data <- Convertible.model_to_as(event),
audience <-
Audience.calculate_to_and_cc_from_mentions(event),
create_data <-
make_create_data(event_as_data, Map.merge(audience, additional)) do
{:ok, event, create_data}
end
end
@spec create_comment(map(), map()) :: {:ok, map()}
defp create_comment(args, additional) do
with args <- prepare_args_for_comment(args),
{:ok, %Comment{} = comment} <- Conversations.create_comment(args),
comment_as_data <- Convertible.model_to_as(comment),
audience <-
Audience.calculate_to_and_cc_from_mentions(comment),
create_data <-
make_create_data(comment_as_data, Map.merge(audience, additional)) do
{:ok, comment, create_data}
end
end
@spec create_group(map(), map()) :: {:ok, map()}
defp create_group(args, additional) do
with args <- prepare_args_for_group(args),
{:ok, %Actor{type: :Group} = group} <- Actors.create_group(args),
group_as_data <- Convertible.model_to_as(group),
audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []},
create_data <-
make_create_data(group_as_data, Map.merge(audience, additional)) do
{:ok, group, create_data}
end
end
@spec create_todo_list(map(), map()) :: {:ok, map()}
defp create_todo_list(args, additional) do
with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}),
audience <- %{"to" => [group.url], "cc" => []},
create_data <-
make_create_data(todo_list_as_data, Map.merge(audience, additional)) do
{:ok, todo_list, create_data}
end
end
@spec create_todo(map(), map()) :: {:ok, map()}
defp create_todo(args, additional) do
with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <-
Todos.create_todo(args),
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
%Actor{} = creator <- Actors.get_actor(creator_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator},
todo_as_data <-
Convertible.model_to_as(todo),
audience <- %{"to" => [group.url], "cc" => []},
create_data <-
make_create_data(todo_as_data, Map.merge(audience, additional)) do
{:ok, todo, create_data}
end
end
defp create_resource(%{type: type} = args, additional) do
args =
case type do
:folder ->
args
_ ->
case Parser.parse(Map.get(args, :resource_url)) do
{:ok, metadata} ->
Map.put(args, :metadata, metadata)
_ ->
args
end
end
with {:ok,
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <-
Resources.create_resource(args),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
resource_as_data <-
Convertible.model_to_as(%{resource | actor: group, creator: creator}),
audience <- %{
"to" => [group.url],
"cc" => [],
"actor" => creator_url,
"attributedTo" => [creator_url]
} do
create_data =
case parent_id do
nil ->
make_create_data(resource_as_data, Map.merge(audience, additional))
parent_id ->
# In case the resource has a parent we don't `Create` the resource but `Add` it to an existing resource
parent = Resources.get_resource(parent_id)
make_add_data(resource_as_data, parent, Map.merge(audience, additional))
end
{:ok, resource, create_data}
else
err ->
Logger.error(inspect(err))
err
end
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 update_event(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any()
defp update_event(%Event{} = old_event, args, additional) do
with args <- prepare_args_for_event(args),
{:ok, %Event{} = new_event} <- Events.update_event(old_event, args),
{:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"),
event_as_data <- Convertible.model_to_as(new_event),
audience <-
Audience.calculate_to_and_cc_from_mentions(new_event),
update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do
{:ok, new_event, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
@spec update_comment(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any()
defp update_comment(%Comment{} = old_comment, args, additional) do
with args <- prepare_args_for_comment(args),
{:ok, %Comment{} = new_comment} <- Conversations.update_comment(old_comment, args),
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
comment_as_data <- Convertible.model_to_as(new_comment),
audience <-
Audience.calculate_to_and_cc_from_mentions(new_comment),
update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do
{:ok, new_comment, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
@spec update_actor(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any
defp update_actor(%Actor{} = old_actor, args, additional) do
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
actor_as_data <- Convertible.model_to_as(new_actor),
{:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"),
audience <-
Audience.calculate_to_and_cc_from_mentions(new_actor),
additional <- Map.merge(additional, %{"actor" => old_actor.url}),
update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do
{:ok, new_actor, update_data}
end
end
@spec update_todo(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any
defp update_todo(%Todo{} = old_todo, args, additional) do
with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args),
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
todo_as_data <-
Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}),
audience <- %{"to" => [group.url], "cc" => []},
update_data <-
make_update_data(todo_as_data, Map.merge(audience, additional)) do
{:ok, todo, update_data}
end
end
defp update_resource(%Resource{} = old_resource, %{parent_id: _parent_id} = args, additional) do
move_resource(old_resource, args, additional)
end
# Simple rename
defp update_resource(%Resource{} = old_resource, %{title: title} = _args, additional) do
with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <-
Resources.update_resource(old_resource, %{title: title}),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
resource_as_data <-
Convertible.model_to_as(%{resource | actor: group}),
audience <- %{
"to" => [group.url],
"cc" => [],
"actor" => creator_url,
"attributedTo" => [creator_url]
},
update_data <-
make_update_data(resource_as_data, Map.merge(audience, additional)) do
{:ok, resource, update_data}
else
err ->
Logger.error(inspect(err))
err
end
end
defp move_resource(
%Resource{parent_id: old_parent_id} = old_resource,
%{parent_id: _new_parent_id} = args,
additional
) do
with {:ok,
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} =
resource} <-
Resources.update_resource(old_resource, args),
old_parent <- Resources.get_resource(old_parent_id),
new_parent <- Resources.get_resource(new_parent_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
resource_as_data <-
Convertible.model_to_as(%{resource | actor: group}),
audience <- %{
"to" => [group.url],
"cc" => [],
"actor" => creator_url,
"attributedTo" => [creator_url]
},
move_data <-
make_move_data(
resource_as_data,
old_parent,
new_parent,
Map.merge(audience, additional)
) do
{:ok, resource, move_data}
else
err ->
Logger.error(inspect(err))
err
end
end
@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}),
@@ -1254,138 +869,4 @@ defmodule Mobilizon.Federation.ActivityPub do
err
end
end
# Prepare and sanitize arguments for events
defp prepare_args_for_event(args) do
# If title is not set: we are not updating it
args =
if Map.has_key?(args, :title) && !is_nil(args.title),
do: Map.update(args, :title, "", &String.trim/1),
else: args
# If we've been given a description (we might not get one if updating)
# sanitize it, HTML it, and extract tags & mentions from it
args =
if Map.has_key?(args, :description) && !is_nil(args.description) do
{description, mentions, tags} =
APIUtils.make_content_html(
String.trim(args.description),
Map.get(args, :tags, []),
"text/html"
)
mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions)
Map.merge(args, %{
description: description,
mentions: mentions,
tags: tags
})
else
args
end
# Check that we can only allow anonymous participation if our instance allows it
{_, options} =
Map.get_and_update(
Map.get(args, :options, %{anonymous_participation: false}),
:anonymous_participation,
fn value ->
{value, value && Mobilizon.Config.anonymous_participation?()}
end
)
args = Map.put(args, :options, options)
Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1)
end
# Prepare and sanitize arguments for comments
defp prepare_args_for_comment(args) do
with in_reply_to_comment <-
args |> Map.get(:in_reply_to_comment_id) |> Conversations.get_comment_with_preload(),
event <- args |> Map.get(:event_id) |> handle_event_for_comment(),
args <- Map.update(args, :visibility, :public, & &1),
{text, mentions, tags} <-
APIUtils.make_content_html(
args |> Map.get(:text, "") |> String.trim(),
# Can't put additional tags on a comment
[],
"text/html"
),
tags <- ConverterUtils.fetch_tags(tags),
mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions),
args <-
Map.merge(args, %{
actor_id: Map.get(args, :actor_id),
text: text,
mentions: mentions,
tags: tags,
event: event,
in_reply_to_comment: in_reply_to_comment,
in_reply_to_comment_id:
if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id)),
origin_comment_id:
if(is_nil(in_reply_to_comment),
do: nil,
else: Comment.get_thread_id(in_reply_to_comment)
)
}) do
args
end
end
@spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil
defp handle_event_for_comment(event_id) when not is_nil(event_id) do
case Events.get_event_with_preload(event_id) do
{:ok, %Event{} = event} -> event
{:error, :event_not_found} -> nil
end
end
defp handle_event_for_comment(nil), do: nil
defp prepare_args_for_group(args) do
with preferred_username <-
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
summary <- args |> Map.get(:summary, "") |> String.trim(),
{summary, _mentions, _tags} <-
summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do
%{args | preferred_username: preferred_username, summary: summary}
end
end
defp prepare_args_for_report(args) do
with {:reporter, %Actor{} = reporter_actor} <-
{:reporter, Actors.get_actor!(args.reporter_id)},
{:reported, %Actor{} = reported_actor} <-
{:reported, Actors.get_actor!(args.reported_id)},
content <- HTML.strip_tags(args.content),
event <- Conversations.get_comment(Map.get(args, :event_id)),
{:get_report_comments, comments} <-
{:get_report_comments,
Conversations.list_comments_by_actor_and_ids(
reported_actor.id,
Map.get(args, :comments_ids, [])
)} do
Map.merge(args, %{
reporter: reporter_actor,
reported: reported_actor,
content: content,
event: event,
comments: comments
})
end
end
defp check_for_actor_key_rotation(%Actor{} = actor) do
if Actors.should_rotate_actor_key(actor) do
Actors.schedule_key_rotation(
actor,
Application.get_env(:mobilizon, :activitypub)[:actor_key_rotation_delay]
)
end
:ok
end
end

View File

@@ -5,7 +5,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Conversations.Comment
alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Share
alias Mobilizon.Storage.Repo
@@ -79,6 +79,14 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
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 mentioned_actors <- Enum.map(comment.mentions, &process_mention/1),
addressed_actors <- get_addressed_actors(mentioned_actors, nil),
@@ -96,6 +104,28 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
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
}) do
case visibility do
:public ->
%{"to" => [members_url, @ap_public], "cc" => []}
:unlisted ->
%{"to" => [members_url], "cc" => [@ap_public]}
:private ->
%{"to" => [members_url], "cc" => []}
end
end
def calculate_to_and_cc_from_mentions(%Event{} = event) do
with mentioned_actors <- Enum.map(event.mentions, &process_mention/1),
addressed_actors <- get_addressed_actors(mentioned_actors, nil),

View File

@@ -0,0 +1,74 @@
defmodule Mobilizon.Federation.ActivityPub.Fetcher do
@moduledoc """
Module to handle direct URL ActivityPub fetches to remote content
If you need to first get cached data, see `Mobilizon.Federation.ActivityPub.fetch_object_from_url/2`
"""
require Logger
alias Mobilizon.Federation.HTTPSignatures.Signature
alias Mobilizon.Federation.ActivityPub.{Relay, Transmogrifier}
alias Mobilizon.Service.HTTP.ActivityPub, as: ActivityPubClient
import Mobilizon.Federation.ActivityPub.Utils,
only: [maybe_date_fetch: 2, sign_fetch: 4, origin_check?: 2]
@spec fetch(String.t(), Keyword.t()) :: {:ok, map()}
def fetch(url, options \\ []) do
on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor())
with date <- Signature.generate_date_header(),
headers <-
[{:Accept, "application/activity+json"}]
|> maybe_date_fetch(date)
|> sign_fetch(on_behalf_of, url, date),
client <-
ActivityPubClient.client(headers: headers),
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <-
ActivityPubClient.get(client, url) do
{:ok, data}
end
end
@spec fetch_and_create(String.t(), Keyword.t()) :: {:ok, map(), struct()}
def fetch_and_create(url, options \\ []) do
with {:ok, data} when is_map(data) <- fetch(url, options),
:ok <- Logger.debug("inspect body from fetch_object_from_url #{url}"),
:ok <- Logger.debug(inspect(data)),
{:origin_check, true} <- {:origin_check, origin_check?(url, data)},
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["attributedTo"] || data["actor"],
"object" => data
} do
Transmogrifier.handle_incoming(params)
else
{:origin_check, false} ->
Logger.warn("Object origin check failed")
{:error, "Object origin check failed"}
end
end
@spec fetch_and_update(String.t(), Keyword.t()) :: {:ok, map(), struct()}
def fetch_and_update(url, options \\ []) do
with {:ok, data} when is_map(data) <- fetch(url, options),
:ok <- Logger.debug("inspect body from fetch_object_from_url #{url}"),
:ok <- Logger.debug(inspect(data)),
{:origin_check, true} <- {:origin_check, origin_check?(url, data)},
params <- %{
"type" => "Update",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["attributedTo"] || data["actor"],
"object" => data
} do
Transmogrifier.handle_incoming(params)
else
{:origin_check, false} ->
Logger.warn("Object origin check failed")
{:error, "Object origin check failed"}
end
end
end

View File

@@ -0,0 +1,30 @@
defmodule Mobilizon.Federation.ActivityPub.Preloader do
@moduledoc """
Module to ensure entities are correctly preloaded
"""
# TODO: Move me in a more appropriate place
alias Mobilizon.{Actors, Discussions, Events, Resources}
alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.Event
alias Mobilizon.Resources.Resource
alias Mobilizon.Tombstone
def maybe_preload(%Event{url: url}),
do: {:ok, Events.get_public_event_by_url_with_preload!(url)}
def maybe_preload(%Comment{url: url}),
do: {:ok, Discussions.get_comment_from_url_with_preload!(url)}
def maybe_preload(%Discussion{} = discussion), do: {:ok, discussion}
def maybe_preload(%Resource{url: url}),
do: {:ok, Resources.get_resource_by_url_with_preloads(url)}
def maybe_preload(%Actor{url: url}), do: {:ok, Actors.get_actor_by_url!(url, true)}
def maybe_preload(%Tombstone{uri: _uri} = tombstone), do: {:ok, tombstone}
def maybe_preload(other), do: {:error, other}
end

View File

@@ -3,24 +3,31 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
Module that provides functions to explore and fetch collections on a group
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityStream.Converter.Member, as: MemberConverter
alias Mobilizon.Federation.ActivityStream.Converter.Resource, as: ResourceConverter
alias Mobilizon.Federation.HTTPSignatures.Signature
alias Mobilizon.Resources
alias Mobilizon.Federation.ActivityPub.{Fetcher, Transmogrifier}
require Logger
import Mobilizon.Federation.ActivityPub.Utils,
only: [maybe_date_fetch: 2, sign_fetch: 4]
@spec fetch_group(String.t(), Actor.t()) :: :ok
def fetch_group(group_url, %Actor{} = on_behalf_of) do
with {:ok, %Actor{resources_url: resources_url, members_url: members_url}} <-
with {:ok,
%Actor{
outbox_url: outbox_url,
resources_url: resources_url,
members_url: members_url,
posts_url: posts_url,
todos_url: todos_url,
discussions_url: discussions_url,
events_url: events_url
}} <-
ActivityPub.get_or_fetch_actor_by_url(group_url) do
fetch_collection(outbox_url, on_behalf_of)
fetch_collection(members_url, on_behalf_of)
fetch_collection(resources_url, on_behalf_of)
fetch_collection(posts_url, on_behalf_of)
fetch_collection(todos_url, on_behalf_of)
fetch_collection(discussions_url, on_behalf_of)
fetch_collection(events_url, on_behalf_of)
end
end
@@ -30,12 +37,28 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
Logger.debug("Fetching and preparing collection from url")
Logger.debug(inspect(collection_url))
with {:ok, data} <- fetch(collection_url, on_behalf_of) do
with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of) do
Logger.debug("Fetch ok, passing to process_collection")
process_collection(data, on_behalf_of)
end
end
@spec fetch_element(String.t(), Actor.t()) :: any()
def fetch_element(url, %Actor{} = on_behalf_of) do
with {:ok, data} <- Fetcher.fetch(url, on_behalf_of: on_behalf_of) do
case handling_element(data) do
{:ok, _activity, entity} ->
{:ok, entity}
{:ok, entity} ->
{:ok, entity}
err ->
{:error, err}
end
end
end
defp process_collection(%{"type" => type, "orderedItems" => items}, _on_behalf_of)
when type in ["OrderedCollection", "OrderedCollectionPage"] do
Logger.debug(
@@ -55,55 +78,26 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
when is_bitstring(first) do
Logger.debug("OrderedCollection has a first property pointing to an URI")
with {:ok, data} <- fetch(first, on_behalf_of) do
with {:ok, data} <- Fetcher.fetch(first, on_behalf_of: on_behalf_of) do
Logger.debug("Fetched the collection for first property")
process_collection(data, on_behalf_of)
end
end
defp handling_element(%{"type" => "Member"} = data) do
Logger.debug("Handling Member element")
defp handling_element(data) when is_map(data) do
activity = %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["actor"],
"attributedTo" => data["attributedTo"],
"object" => data
}
data
|> MemberConverter.as_to_model_data()
|> Actors.create_member()
Transmogrifier.handle_incoming(activity)
end
defp handling_element(%{"type" => type} = data)
when type in ["Document", "ResourceCollection"] do
Logger.debug("Handling Resource element")
data
|> ResourceConverter.as_to_model_data()
|> Resources.create_resource()
end
defp fetch(url, %Actor{} = on_behalf_of) do
with date <- Signature.generate_date_header(),
headers <-
[{:Accept, "application/activity+json"}]
|> maybe_date_fetch(date)
|> sign_fetch(on_behalf_of, url, date),
%HTTPoison.Response{status_code: 200, body: body} <-
HTTPoison.get!(url, headers,
follow_redirect: true,
ssl: [{:versions, [:"tlsv1.2"]}]
),
{:ok, data} <-
Jason.decode(body) do
{:ok, data}
else
# Actor is gone, probably deleted
{:ok, %HTTPoison.Response{status_code: 410}} ->
Logger.info("Response HTTP 410")
{:error, :actor_deleted}
{:origin_check, false} ->
{:error, "Origin check failed"}
e ->
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
{:error, e}
end
defp handling_element(uri) when is_binary(uri) do
ActivityPub.fetch_object_from_url(uri)
end
end

View File

@@ -8,17 +8,19 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Mobilizon.{Actors, Conversations, Events, Resources, Todos}
alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos}
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Conversations.Comment
alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Posts.Post
alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
alias Mobilizon.Federation.ActivityPub.{Activity, Relay, Utils}
alias Mobilizon.Federation.ActivityPub.Types.Ownable
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Tombstone
alias Mobilizon.Web.Email.{Group, Participation}
require Logger
@@ -62,10 +64,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
with object_data when is_map(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}
{:existing_comment, Discussions.get_comment_from_url_with_preload(object_data.url)},
object_data <- transform_object_data_for_discussion(object_data) do
# Check should be better
{:ok, %Activity{} = activity, entity} =
if is_data_for_comment_or_discussion?(object_data) do
Logger.debug("Chosing to create a regular comment")
ActivityPub.create(:comment, object_data, false)
else
Logger.debug("Chosing to initialize or add a comment to a conversation")
ActivityPub.create(:discussion, object_data, false)
end
{:ok, activity, entity}
else
{:existing_comment, {:ok, %Comment{} = comment}} ->
{:ok, nil, comment}
@@ -100,6 +112,77 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
def handle_incoming(%{
"type" => "Create",
"object" => %{"type" => "Group", "id" => group_url} = _object
}) do
Logger.info("Handle incoming to create a group")
with {:ok, %Actor{} = group} <- ActivityPub.get_or_fetch_actor_by_url(group_url) do
{:ok, nil, group}
end
end
def handle_incoming(%{
"type" => "Create",
"object" => %{"type" => "Member"} = object
}) do
Logger.info("Handle incoming to create a member")
with object_data when is_map(object_data) <-
object |> Converter.Member.as_to_model_data(),
{:existing_member, nil} <-
{:existing_member, Actors.get_member_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Member{} = member} <-
ActivityPub.join_group(object_data, false) do
{:ok, activity, member}
else
{:existing_member, %Member{} = member} ->
{:ok, nil, member}
end
end
def handle_incoming(%{
"type" => "Create",
"object" =>
%{"type" => "Article", "actor" => _actor, "attributedTo" => _attributed_to} = object
}) do
Logger.info("Handle incoming to create articles")
with object_data when is_map(object_data) <-
object |> Converter.Post.as_to_model_data(),
{:existing_post, nil} <-
{:existing_post, Posts.get_post_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Post{} = post} <-
ActivityPub.create(:post, object_data, false) do
{:ok, activity, post}
else
{:existing_post, %Post{} = post} ->
{:ok, nil, post}
end
end
# This is a hack to handle Tombstones fetched by AP
def handle_incoming(%{
"type" => "Create",
"object" => %{"type" => "Tombstone", "id" => object_url} = _object
}) do
Logger.info("Handle incoming to create a tombstone")
case ActivityPub.fetch_object_from_url(object_url, force: true) do
# We already have the tombstone, object is probably already deleted
{:ok, %Tombstone{} = tombstone} ->
{:ok, nil, tombstone}
# Hack because deleted comments
{:ok, %Comment{deleted_at: deleted_at} = comment} when not is_nil(deleted_at) ->
{:ok, nil, comment}
{:ok, entity} ->
ActivityPub.delete(entity, Relay.get_actor(), false)
end
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
) do
@@ -165,7 +248,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Logger.info("Handle incoming to create a resource")
Logger.debug(inspect(data))
group_url = hd(to)
group_url = if is_list(to) and not is_nil(to), do: hd(to), else: to
with {:existing_resource, nil} <-
{:existing_resource, Resources.get_resource_by_url(object_url)},
@@ -175,8 +258,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{: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} <-
ActivityPub.get_or_fetch_actor_by_url(group_url),
%Actor{type: :Group, id: group_id} = group <-
Actors.get_group_by_members_url(group_url),
announce_id <- "#{object_url}/announces/#{group_id}",
{:ok, _activity, _resource} <- ActivityPub.announce(group, object, announce_id) do
{:ok, activity, resource}
@@ -190,7 +273,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
:error
{:error, e} ->
Logger.error(inspect(e))
Logger.debug(inspect(e))
:error
end
end
@@ -261,23 +344,14 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
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, suspended: false} = _actor} <-
ActivityPub.get_or_fetch_actor_by_url(actor),
with actor_url <- Utils.get_actor(data),
{:ok, %Actor{id: actor_id, suspended: false} = actor} <-
ActivityPub.get_or_fetch_actor_by_url(actor_url),
: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}} <-
ActivityPub.get_or_fetch_actor_by_url(object["actor"]),
{:ok, %Mobilizon.Share{} = _share} <-
Mobilizon.Share.create(object["id"], actor_id, object_owner_actor_id) do
{:ok, activity, entity}
{:ok, entity} <-
object |> Utils.get_url() |> fetch_object_optionnally_authenticated(actor),
:ok <- eventually_create_share(object, entity, actor_id) do
{:ok, nil, entity}
else
e ->
Logger.debug(inspect(e))
@@ -296,7 +370,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data <-
object |> Converter.Actor.as_to_model_data(),
{:ok, %Activity{} = activity, %Actor{} = new_actor} <-
ActivityPub.update(:actor, old_actor, object_data, false) do
ActivityPub.update(old_actor, object_data, false) do
{:ok, activity, new_actor}
else
e ->
@@ -317,7 +391,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
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
ActivityPub.update(old_event, object_data, false) do
{:ok, activity, new_event}
else
_e ->
@@ -325,6 +399,42 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Note"} = object, "actor" => _actor} =
update_data
) do
with actor <- Utils.get_actor(update_data),
{:ok, %Actor{url: actor_url, suspended: false}} <-
ActivityPub.get_or_fetch_actor_by_url(actor),
{:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)},
object_data <- Converter.Comment.as_to_model_data(object),
{:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
object_data <- transform_object_data_for_discussion(object_data),
{:ok, %Activity{} = activity, new_entity} <-
ActivityPub.update(old_entity, object_data, false) do
{:ok, activity, new_entity}
else
_e ->
:error
end
end
def handle_incoming(%{
"type" => "Update",
"object" => %{"type" => "Tombstone"} = object,
"actor" => _actor
}) do
Logger.info("Handle incoming to update a tombstone")
with object_url <- Utils.get_url(object),
{:ok, entity} <- ActivityPub.fetch_object_from_url(object_url) do
ActivityPub.delete(entity, Relay.get_actor(), false)
else
{:ok, %Tombstone{} = tombstone} ->
{:ok, nil, tombstone}
end
end
def handle_incoming(
%{
"type" => "Undo",
@@ -367,21 +477,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
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.
# We assume everyone on the same instance as the object
# or who is member of a group has the right to delete the object
def handle_incoming(
%{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data
) do
with actor <- Utils.get_actor(data),
{:ok, %Actor{url: actor_url}} <- ActivityPub.get_or_fetch_actor_by_url(actor),
with actor_url <- Utils.get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
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
{:origin_check, true} <-
{:origin_check,
Utils.origin_check_from_id?(actor_url, object_id) ||
Utils.activity_actor_is_group_member?(actor, object)},
{:ok, activity, object} <- ActivityPub.delete(object, actor, false) do
{:ok, activity, object}
else
{:origin_check, false} ->
@@ -449,6 +558,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
"target" => target
} = data
) do
Logger.info("Handle incoming to invite someone")
with {:ok, %Actor{} = actor} <-
data |> Utils.get_actor() |> ActivityPub.get_or_fetch_actor_by_url(),
{:ok, object} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
@@ -485,7 +596,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
# end
def handle_incoming(object) do
Logger.info("Handing something not supported")
Logger.info("Handing something with type #{object["type"]} not supported")
Logger.debug(inspect(object))
{:error, :not_supported}
end
@@ -657,6 +768,52 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
# If the object has been announced by a group let's use one of our members to fetch it
@spec fetch_object_optionnally_authenticated(String.t(), Actor.t() | any()) ::
{:ok, struct()} | {:error, any()}
defp fetch_object_optionnally_authenticated(url, %Actor{type: :Group, id: group_id}) do
case Actors.get_single_group_member_actor(group_id) do
%Actor{} = actor ->
ActivityPub.fetch_object_from_url(url, on_behalf_of: actor, force: true)
_err ->
fetch_object_optionnally_authenticated(url, nil)
end
end
defp fetch_object_optionnally_authenticated(url, _),
do: ActivityPub.fetch_object_from_url(url, force: true)
defp eventually_create_share(object, entity, actor_id) do
with object_id <- object |> Utils.get_url(),
%Actor{id: object_owner_actor_id} <- Ownable.actor(entity) do
{:ok, %Mobilizon.Share{} = _share} =
Mobilizon.Share.create(object_id, actor_id, object_owner_actor_id)
end
:ok
end
@spec is_data_for_comment_or_discussion?(map()) :: boolean()
defp is_data_for_comment_or_discussion?(object_data) do
(not Map.has_key?(object_data, :title) or
is_nil(object_data.title) or object_data.title == "") and
is_nil(object_data.discussion_id)
end
# Comment and conversations have different attributes for actor and groups
defp transform_object_data_for_discussion(object_data) do
# Basic comment
if is_data_for_comment_or_discussion?(object_data) do
object_data
else
# Conversation
object_data
|> Map.put(:creator_id, object_data.actor_id)
|> Map.put(:actor_id, object_data.attributed_to_id)
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} <-

View File

@@ -0,0 +1,74 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
@moduledoc false
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Audience
alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
alias Mobilizon.Service.Formatter.HTML
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
@behaviour Entity
@impl Entity
@spec create(map(), map()) :: {:ok, map()}
def create(args, additional) do
with args <- prepare_args_for_actor(args),
{:ok, %Actor{} = actor} <- Actors.create_actor(args),
actor_as_data <- Convertible.model_to_as(actor),
audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []},
create_data <-
make_create_data(actor_as_data, Map.merge(audience, additional)) do
{:ok, actor, create_data}
end
end
@impl Entity
@spec update(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any
def update(%Actor{} = old_actor, args, additional) do
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
actor_as_data <- Convertible.model_to_as(new_actor),
{:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"),
audience <-
Audience.calculate_to_and_cc_from_mentions(new_actor),
additional <- Map.merge(additional, %{"actor" => old_actor.url}),
update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do
{:ok, new_actor, update_data}
end
end
@impl Entity
def delete(
%Actor{followers_url: followers_url, url: target_actor_url} = target_actor,
%Actor{url: actor_url} = actor,
local
) do
activity_data = %{
"type" => "Delete",
"actor" => actor_url,
"object" => Convertible.model_to_as(target_actor),
"id" => target_actor_url <> "/delete",
"to" => [followers_url, "https://www.w3.org/ns/activitystreams#Public"]
}
# We completely delete the actor if activity is remote
with {:ok, %Oban.Job{}} <- Actors.delete_actor(target_actor, reserve_username: local) do
{:ok, activity_data, actor, target_actor}
end
end
def actor(%Actor{} = actor), do: actor
def group_actor(%Actor{} = _actor), do: nil
defp prepare_args_for_actor(args) do
with preferred_username <-
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
summary <- args |> Map.get(:summary, "") |> String.trim(),
{summary, _mentions, _tags} <-
summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do
%{args | preferred_username: preferred_username, summary: summary}
end
end
end

View File

@@ -0,0 +1,149 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
@moduledoc false
alias Mobilizon.{Actors, Discussions, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub.Audience
alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
alias Mobilizon.Share
alias Mobilizon.Tombstone
alias Mobilizon.Web.Endpoint
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
require Logger
@behaviour Entity
@impl Entity
@spec create(map(), map()) :: {:ok, map()}
def create(args, additional) do
with args <- prepare_args_for_comment(args),
{:ok, %Comment{discussion_id: discussion_id} = comment} <-
Discussions.create_comment(args),
:ok <- maybe_publish_graphql_subscription(discussion_id),
comment_as_data <- Convertible.model_to_as(comment),
audience <-
Audience.calculate_to_and_cc_from_mentions(comment),
create_data <-
make_create_data(comment_as_data, Map.merge(audience, additional)) do
{:ok, comment, create_data}
end
end
@impl Entity
@spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any()
def update(%Comment{} = old_comment, args, additional) do
with args <- prepare_args_for_comment(args),
{:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args),
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
comment_as_data <- Convertible.model_to_as(new_comment),
audience <-
Audience.calculate_to_and_cc_from_mentions(new_comment),
update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do
{:ok, new_comment, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
@impl Entity
@spec delete(Comment.t(), Actor.t(), boolean) :: {:ok, Comment.t()}
def delete(%Comment{url: url} = comment, %Actor{} = actor, _local) do
activity_data = %{
"type" => "Delete",
"actor" => actor.url,
"object" => Convertible.model_to_as(comment),
"id" => url <> "/delete",
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
with audience <-
Audience.calculate_to_and_cc_from_mentions(comment),
{:ok, %Comment{} = comment} <- Discussions.delete_comment(comment),
# Preload to be sure
%Comment{} = comment <- Discussions.get_comment_with_preload(comment.id),
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
{:ok, %Tombstone{} = _tombstone} <-
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}) do
Share.delete_all_by_uri(comment.url)
{:ok, Map.merge(activity_data, audience), actor, comment}
end
end
def actor(%Comment{actor: %Actor{} = actor}), do: actor
def actor(%Comment{actor_id: actor_id}) when not is_nil(actor_id),
do: Actors.get_actor(actor_id)
def actor(_), do: nil
def group_actor(%Comment{attributed_to: %Actor{} = group}), do: group
def group_actor(%Comment{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
do: Actors.get_actor(attributed_to_id)
def group_actor(_), do: nil
# Prepare and sanitize arguments for comments
defp prepare_args_for_comment(args) do
with in_reply_to_comment <-
args |> Map.get(:in_reply_to_comment_id) |> Discussions.get_comment_with_preload(),
event <- args |> Map.get(:event_id) |> handle_event_for_comment(),
args <- Map.update(args, :visibility, :public, & &1),
{text, mentions, tags} <-
APIUtils.make_content_html(
args |> Map.get(:text, "") |> String.trim(),
# Can't put additional tags on a comment
[],
"text/html"
),
tags <- ConverterUtils.fetch_tags(tags),
mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions),
args <-
Map.merge(args, %{
actor_id: Map.get(args, :actor_id),
text: text,
mentions: mentions,
tags: tags,
event: event,
in_reply_to_comment: in_reply_to_comment,
in_reply_to_comment_id:
if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id)),
origin_comment_id:
if(is_nil(in_reply_to_comment),
do: nil,
else: Comment.get_thread_id(in_reply_to_comment)
)
}) do
args
end
end
@spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil
defp handle_event_for_comment(event_id) when not is_nil(event_id) do
case Events.get_event_with_preload(event_id) do
{:ok, %Event{} = event} -> event
{:error, :event_not_found} -> nil
end
end
defp handle_event_for_comment(nil), do: nil
defp maybe_publish_graphql_subscription(nil), do: :ok
defp maybe_publish_graphql_subscription(discussion_id) do
with %Discussion{} = discussion <- Discussions.get_discussion(discussion_id) do
Absinthe.Subscription.publish(Endpoint, discussion,
discussion_comment_changed: discussion.slug
)
:ok
end
end
end

View File

@@ -0,0 +1,115 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
@moduledoc false
alias Mobilizon.{Actors, Discussions}
alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Federation.ActivityPub.Audience
alias Mobilizon.Federation.ActivityPub.Types.{Comments, Entity}
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Storage.Repo
alias Mobilizon.Web.Endpoint
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
require Logger
@behaviour Entity
@impl Entity
@spec create(map(), map()) :: {:ok, map()}
def create(%{discussion_id: discussion_id} = args, additional) when not is_nil(discussion_id) do
with %Discussion{} = discussion <- Discussions.get_discussion(discussion_id),
{:ok, %Discussion{last_comment_id: last_comment_id} = discussion} <-
Discussions.reply_to_discussion(discussion, args),
%Comment{} = last_comment <- Discussions.get_comment_with_preload(last_comment_id),
:ok <- maybe_publish_graphql_subscription(discussion),
comment_as_data <- Convertible.model_to_as(last_comment),
audience <-
Audience.calculate_to_and_cc_from_mentions(discussion),
create_data <-
make_create_data(comment_as_data, Map.merge(audience, additional)) do
{:ok, discussion, create_data}
end
end
@impl Entity
@spec create(map(), map()) :: {:ok, map()}
def create(args, additional) do
with {:ok, %Discussion{} = discussion} <-
Discussions.create_discussion(args),
discussion_as_data <- Convertible.model_to_as(discussion),
audience <-
Audience.calculate_to_and_cc_from_mentions(discussion),
create_data <-
make_create_data(discussion_as_data, Map.merge(audience, additional)) do
{:ok, discussion, create_data}
end
end
@impl Entity
@spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), Activity.t()} | any()
def update(%Discussion{} = old_discussion, args, additional) do
with {:ok, %Discussion{} = new_discussion} <-
Discussions.update_discussion(old_discussion, args),
{:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"),
discussion_as_data <- Convertible.model_to_as(new_discussion),
audience <-
Audience.calculate_to_and_cc_from_mentions(new_discussion),
update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do
{:ok, new_discussion, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
@impl Entity
@spec delete(Discussion.t(), Actor.t(), boolean) :: {:ok, Discussion.t()}
def delete(%Discussion{actor: group, url: url} = discussion, %Actor{} = actor, _local) do
stream =
discussion.comments
|> Enum.map(
&Repo.preload(&1, [
:actor,
:attributed_to,
:in_reply_to_comment,
:mentions,
:origin_comment,
:discussion,
:tags,
:replies
])
)
|> Enum.map(&Map.put(&1, :event, nil))
|> Task.async_stream(fn comment -> Comments.delete(comment, actor, nil) end)
Stream.run(stream)
with {:ok, %Discussion{}} <- Discussions.delete_discussion(discussion) do
# This is just fake
activity_data = %{
"type" => "Delete",
"actor" => actor.url,
"object" => Convertible.model_to_as(discussion),
"id" => url <> "/delete",
"to" => [group.members_url]
}
{:ok, activity_data, actor, discussion}
end
end
def actor(%Discussion{creator_id: creator_id}), do: Actors.get_actor(creator_id)
def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id)
@spec maybe_publish_graphql_subscription(Discussion.t()) :: :ok
defp maybe_publish_graphql_subscription(%Discussion{} = discussion) do
Absinthe.Subscription.publish(Endpoint, discussion,
discussion_comment_changed: discussion.slug
)
:ok
end
end

View File

@@ -0,0 +1,151 @@
alias Mobilizon.Federation.ActivityPub.Types.{
Actors,
Comments,
Discussions,
Entity,
Events,
Managable,
Ownable,
Posts,
Resources,
Todos,
TodoLists,
Tombstones
}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Posts.Post
alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList}
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Tombstone
defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
@moduledoc """
ActivityPub entity behaviour
"""
@type t :: %{id: String.t()}
@callback create(data :: any(), additionnal :: map()) ::
{:ok, t(), ActivityStream.t()}
@callback update(struct :: t(), attrs :: map(), additionnal :: map()) ::
{:ok, t(), ActivityStream.t()}
@callback delete(struct :: t(), Actor.t(), local :: boolean()) ::
{:ok, ActivityStream.t(), Actor.t(), t()}
end
defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
@moduledoc """
ActivityPub entity Managable protocol.
"""
@spec update(Entity.t(), map(), map()) :: {:ok, Entity.t(), ActivityStream.t()}
@doc """
Updates a `Managable` entity with the appropriate attributes and returns the updated entity and an activitystream representation for it
"""
def update(entity, attrs, additionnal)
@spec delete(Entity.t(), Actor.t(), boolean()) ::
{:ok, ActivityStream.t(), Actor.t(), Entity.t()}
@doc "Deletes an entity and returns the activitystream representation for it"
def delete(entity, actor, local)
end
defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
@spec group_actor(Entity.t()) :: Actor.t() | nil
@doc "Returns an eventual group for the entity"
def group_actor(entity)
@spec actor(Entity.t()) :: Actor.t() | nil
@doc "Returns the actor for the entity"
def actor(entity)
end
defimpl Managable, for: Event do
defdelegate update(entity, attrs, additionnal), to: Events
defdelegate delete(entity, actor, local), to: Events
end
defimpl Ownable, for: Event do
defdelegate group_actor(entity), to: Events
defdelegate actor(entity), to: Events
end
defimpl Managable, for: Comment do
defdelegate update(entity, attrs, additionnal), to: Comments
defdelegate delete(entity, actor, local), to: Comments
end
defimpl Ownable, for: Comment do
defdelegate group_actor(entity), to: Comments
defdelegate actor(entity), to: Comments
end
defimpl Managable, for: Post do
defdelegate update(entity, attrs, additionnal), to: Posts
defdelegate delete(entity, actor, local), to: Posts
end
defimpl Ownable, for: Post do
defdelegate group_actor(entity), to: Posts
defdelegate actor(entity), to: Posts
end
defimpl Managable, for: Actor do
defdelegate update(entity, attrs, additionnal), to: Actors
defdelegate delete(entity, actor, local), to: Actors
end
defimpl Ownable, for: Actor do
defdelegate group_actor(entity), to: Actors
defdelegate actor(entity), to: Actors
end
defimpl Managable, for: TodoList do
defdelegate update(entity, attrs, additionnal), to: TodoLists
defdelegate delete(entity, actor, local), to: TodoLists
end
defimpl Ownable, for: TodoList do
defdelegate group_actor(entity), to: TodoLists
defdelegate actor(entity), to: TodoLists
end
defimpl Managable, for: Todo do
defdelegate update(entity, attrs, additionnal), to: Todos
defdelegate delete(entity, actor, local), to: Todos
end
defimpl Ownable, for: Todo do
defdelegate group_actor(entity), to: Todos
defdelegate actor(entity), to: Todos
end
defimpl Managable, for: Resource do
defdelegate update(entity, attrs, additionnal), to: Resources
defdelegate delete(entity, actor, local), to: Resources
end
defimpl Ownable, for: Resource do
defdelegate group_actor(entity), to: Resources
defdelegate actor(entity), to: Resources
end
defimpl Managable, for: Discussion do
defdelegate update(entity, attrs, additionnal), to: Discussions
defdelegate delete(entity, actor, local), to: Discussions
end
defimpl Ownable, for: Discussion do
defdelegate group_actor(entity), to: Discussions
defdelegate actor(entity), to: Discussions
end
defimpl Ownable, for: Tombstone do
defdelegate group_actor(entity), to: Tombstones
defdelegate actor(entity), to: Tombstones
end

View File

@@ -0,0 +1,203 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Events do
@moduledoc false
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events, as: EventsManager
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Audience
alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
alias Mobilizon.Service.Formatter.HTML
alias Mobilizon.Service.Notifications.Scheduler
alias Mobilizon.Share
alias Mobilizon.Tombstone
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
require Logger
@behaviour Entity
@impl Entity
@spec create(map(), map()) :: {:ok, map()}
def create(args, additional) do
with args <- prepare_args_for_event(args),
{:ok, %Event{} = event} <- EventsManager.create_event(args),
event_as_data <- Convertible.model_to_as(event),
audience <-
Audience.calculate_to_and_cc_from_mentions(event),
create_data <-
make_create_data(event_as_data, Map.merge(audience, additional)) do
{:ok, event, create_data}
end
end
@impl Entity
@spec update(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any()
def update(%Event{} = old_event, args, additional) do
with args <- prepare_args_for_event(args),
{:ok, %Event{} = new_event} <- EventsManager.update_event(old_event, args),
{:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"),
event_as_data <- Convertible.model_to_as(new_event),
audience <-
Audience.calculate_to_and_cc_from_mentions(new_event),
update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do
{:ok, new_event, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
@impl Entity
@spec delete(Event.t(), Actor.t(), boolean) :: {:ok, Event.t()}
def delete(%Event{url: url} = event, %Actor{} = actor, _local) do
activity_data = %{
"type" => "Delete",
"actor" => actor.url,
"object" => Convertible.model_to_as(event),
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"],
"id" => url <> "/delete"
}
with audience <-
Audience.calculate_to_and_cc_from_mentions(event),
{:ok, %Event{} = event} <- EventsManager.delete_event(event),
{:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"),
{:ok, %Tombstone{} = _tombstone} <-
Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do
Share.delete_all_by_uri(event.url)
{:ok, Map.merge(activity_data, audience), actor, event}
end
end
def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor
def actor(%Event{organizer_actor_id: organizer_actor_id}),
do: Actors.get_actor(organizer_actor_id)
def actor(_), do: nil
def group_actor(%Event{attributed_to: %Actor{} = group}), do: group
def group_actor(%Event{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
do: Actors.get_actor(attributed_to_id)
def group_actor(_), do: nil
def join(%Event{} = event, %Actor{} = actor, _local, additional) do
with {:maximum_attendee_capacity, true} <-
{:maximum_attendee_capacity, check_attendee_capacity(event)},
role <-
additional
|> Map.get(:metadata, %{})
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)),
{:ok, %Participant{} = participant} <-
Mobilizon.Events.create_participant(%{
role: role,
event_id: event.id,
actor_id: actor.id,
url: Map.get(additional, :url),
metadata:
additional
|> Map.get(:metadata, %{})
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
}),
join_data <- Convertible.model_to_as(participant),
audience <-
Audience.calculate_to_and_cc_from_mentions(participant) do
approve_if_default_role_is_participant(
event,
Map.merge(join_data, audience),
participant,
role
)
else
{:maximum_attendee_capacity, err} ->
{:maximum_attendee_capacity, err}
end
end
defp check_attendee_capacity(%Event{options: options} = event) do
with maximum_attendee_capacity <-
Map.get(options, :maximum_attendee_capacity) || 0 do
maximum_attendee_capacity == 0 ||
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
end
end
# Set the participant to approved if the default role for new participants is :participant
defp approve_if_default_role_is_participant(event, activity_data, participant, role) do
if event.local do
cond do
Mobilizon.Events.get_default_participant_role(event) === :participant &&
role == :participant ->
{:accept,
ActivityPub.accept(
:join,
participant,
true,
%{"actor" => event.organizer_actor.url}
)}
Mobilizon.Events.get_default_participant_role(event) === :not_approved &&
role == :not_approved ->
Scheduler.pending_participation_notification(event)
{:ok, activity_data, participant}
true ->
{:ok, activity_data, participant}
end
else
{:ok, activity_data, participant}
end
end
# Prepare and sanitize arguments for events
defp prepare_args_for_event(args) do
# If title is not set: we are not updating it
args =
if Map.has_key?(args, :title) && !is_nil(args.title),
do: Map.update(args, :title, "", &String.trim/1),
else: args
# If we've been given a description (we might not get one if updating)
# sanitize it, HTML it, and extract tags & mentions from it
args =
if Map.has_key?(args, :description) && !is_nil(args.description) do
{description, mentions, tags} =
APIUtils.make_content_html(
String.trim(args.description),
Map.get(args, :tags, []),
"text/html"
)
mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions)
Map.merge(args, %{
description: description,
mentions: mentions,
tags: tags
})
else
args
end
# Check that we can only allow anonymous participation if our instance allows it
{_, options} =
Map.get_and_update(
Map.get(args, :options, %{anonymous_participation: false}),
:anonymous_participation,
fn value ->
{value, value && Mobilizon.Config.anonymous_participation?()}
end
)
args = Map.put(args, :options, options)
Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1)
end
end

View File

@@ -0,0 +1,93 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
@moduledoc false
alias Mobilizon.{Actors, Posts}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Posts.Post
require Logger
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
@behaviour Entity
@impl Entity
def create(args, additional) do
with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1),
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
Posts.create_post(args),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
post_as_data <-
Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
audience <- %{
"to" => [group.members_url],
"cc" => [],
"actor" => creator_url,
"attributedTo" => [creator_url]
} do
create_data = make_create_data(post_as_data, Map.merge(audience, additional))
{:ok, post, create_data}
else
err ->
Logger.debug(inspect(err))
err
end
end
@impl Entity
def update(%Post{} = post, args, additional) do
with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1),
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
Posts.update_post(post, args),
{:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}"),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
post_as_data <-
Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
audience <- %{
"to" => [group.members_url],
"cc" => [],
"actor" => creator_url,
"attributedTo" => [creator_url]
} do
update_data = make_update_data(post_as_data, Map.merge(audience, additional))
{:ok, post, update_data}
else
err ->
Logger.debug(inspect(err))
err
end
end
@impl Entity
def delete(
%Post{
url: url,
attributed_to: %Actor{url: group_url}
} = post,
%Actor{url: actor_url} = actor,
_local
) do
activity_data = %{
"actor" => actor_url,
"type" => "Delete",
"object" => Convertible.model_to_as(post),
"id" => url <> "/delete",
"to" => [group_url]
}
with {:ok, _post} <- Posts.delete_post(post),
{:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}") do
{:ok, activity_data, actor, post}
end
end
def actor(%Post{author_id: author_id}),
do: Actors.get_actor(author_id)
def group_actor(%Post{attributed_to_id: attributed_to_id}),
do: Actors.get_actor(attributed_to_id)
end

View File

@@ -0,0 +1,43 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
@moduledoc false
alias Mobilizon.{Actors, Discussions, Reports}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Reports.Report
alias Mobilizon.Service.Formatter.HTML
require Logger
def flag(args, local \\ false, _additional \\ %{}) do
with {:build_args, args} <- {:build_args, prepare_args_for_report(args)},
{:create_report, {:ok, %Report{} = report}} <-
{:create_report, Reports.create_report(args)},
report_as_data <- Convertible.model_to_as(report),
cc <- if(local, do: [report.reported.url], else: []),
report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}) do
{report, report_as_data}
end
end
defp prepare_args_for_report(args) do
with {:reporter, %Actor{} = reporter_actor} <-
{:reporter, Actors.get_actor!(args.reporter_id)},
{:reported, %Actor{} = reported_actor} <-
{:reported, Actors.get_actor!(args.reported_id)},
content <- HTML.strip_tags(args.content),
event <- Discussions.get_comment(Map.get(args, :event_id)),
{:get_report_comments, comments} <-
{:get_report_comments,
Discussions.list_comments_by_actor_and_ids(
reported_actor.id,
Map.get(args, :comments_ids, [])
)} do
Map.merge(args, %{
reporter: reporter_actor,
reported: reported_actor,
content: content,
event: event,
comments: comments
})
end
end
end

View File

@@ -0,0 +1,157 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
@moduledoc false
alias Mobilizon.{Actors, Resources}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Resources.Resource
alias Mobilizon.Service.RichMedia.Parser
require Logger
import Mobilizon.Federation.ActivityPub.Utils,
only: [make_create_data: 2, make_update_data: 2, make_add_data: 3, make_move_data: 4]
@behaviour Entity
@impl Entity
def create(%{type: type} = args, additional) do
args =
case type do
:folder ->
args
_ ->
case Parser.parse(Map.get(args, :resource_url)) do
{:ok, metadata} ->
Map.put(args, :metadata, metadata)
_ ->
args
end
end
with {:ok,
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <-
Resources.create_resource(args),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
resource_as_data <-
Convertible.model_to_as(%{resource | actor: group, creator: creator}),
audience <- %{
"to" => [group.members_url],
"cc" => [],
"actor" => creator_url,
"attributedTo" => [creator_url]
} do
create_data =
case parent_id do
nil ->
make_create_data(resource_as_data, Map.merge(audience, additional))
parent_id ->
# In case the resource has a parent we don't `Create` the resource but `Add` it to an existing resource
parent = Resources.get_resource(parent_id)
make_add_data(resource_as_data, parent, Map.merge(audience, additional))
end
{:ok, resource, create_data}
else
err ->
Logger.debug(inspect(err))
err
end
end
@impl Entity
def update(%Resource{} = old_resource, %{parent_id: _parent_id} = args, additional) do
move(old_resource, args, additional)
end
# Simple rename
def update(%Resource{} = old_resource, %{title: title} = _args, additional) do
with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <-
Resources.update_resource(old_resource, %{title: title}),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
resource_as_data <-
Convertible.model_to_as(%{resource | actor: group}),
audience <- %{
"to" => [group.members_url],
"cc" => [],
"actor" => creator_url,
"attributedTo" => [creator_url]
},
update_data <-
make_update_data(resource_as_data, Map.merge(audience, additional)) do
{:ok, resource, update_data}
else
err ->
Logger.debug(inspect(err))
err
end
end
def move(
%Resource{parent_id: old_parent_id} = old_resource,
%{parent_id: _new_parent_id} = args,
additional
) do
with {:ok,
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} =
resource} <-
Resources.update_resource(old_resource, args),
old_parent <- Resources.get_resource(old_parent_id),
new_parent <- Resources.get_resource(new_parent_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
resource_as_data <-
Convertible.model_to_as(%{resource | actor: group}),
audience <- %{
"to" => [group.members_url],
"cc" => [],
"actor" => creator_url,
"attributedTo" => [creator_url]
},
move_data <-
make_move_data(
resource_as_data,
old_parent,
new_parent,
Map.merge(audience, additional)
) do
{:ok, resource, move_data}
else
err ->
Logger.debug(inspect(err))
err
end
end
@impl Entity
def delete(
%Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource,
%Actor{url: actor_url} = actor,
_local
) do
Logger.debug("Building Delete Resource activity")
activity_data = %{
"actor" => actor_url,
"attributedTo" => [group_url],
"type" => "Delete",
"object" => Convertible.model_to_as(resource),
"id" => url <> "/delete",
"to" => [members_url]
}
with {:ok, _resource} <- Resources.delete_resource(resource),
{:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}") do
{:ok, activity_data, actor, resource}
end
end
def actor(%Resource{creator_id: creator_id}),
do: Actors.get_actor(creator_id)
def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id)
end

View File

@@ -0,0 +1,69 @@
defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
@moduledoc false
alias Mobilizon.{Actors, Todos}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Todos.TodoList
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
require Logger
@behaviour Entity
@impl Entity
@spec create(map(), map()) :: {:ok, map()}
def create(args, additional) do
with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}),
audience <- %{"to" => [group.members_url], "cc" => []},
create_data <-
make_create_data(todo_list_as_data, Map.merge(audience, additional)) do
{:ok, todo_list, create_data}
end
end
@impl Entity
@spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), Activity.t()} | any
def update(%TodoList{} = old_todo_list, args, additional) do
with {:ok, %TodoList{actor_id: group_id} = todo_list} <-
Todos.update_todo_list(old_todo_list, args),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
todo_list_as_data <-
Convertible.model_to_as(%{todo_list | actor: group}),
audience <- %{"to" => [group.members_url], "cc" => []},
update_data <-
make_update_data(todo_list_as_data, Map.merge(audience, additional)) do
{:ok, todo_list, update_data}
end
end
@impl Entity
@spec delete(TodoList.t(), Actor.t(), boolean()) ::
{:ok, ActivityStream.t(), Actor.t(), TodoList.t()}
def delete(
%TodoList{url: url, actor: %Actor{url: group_url}} = todo_list,
%Actor{url: actor_url} = actor,
_local
) do
Logger.debug("Building Delete TodoList activity")
activity_data = %{
"actor" => actor_url,
"type" => "Delete",
"object" => Convertible.model_to_as(url),
"id" => url <> "/delete",
"to" => [group_url]
}
with {:ok, _todo_list} <- Todos.delete_todo_list(todo_list),
{:ok, true} <- Cachex.del(:activity_pub, "todo_list_#{todo_list.id}") do
{:ok, activity_data, actor, todo_list}
end
end
def actor(%TodoList{}), do: nil
def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id)
end

View File

@@ -0,0 +1,80 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
@moduledoc false
alias Mobilizon.{Actors, Todos}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Todos.{Todo, TodoList}
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
require Logger
@behaviour Entity
@impl Entity
@spec create(map(), map()) :: {:ok, map()}
def create(args, additional) do
with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <-
Todos.create_todo(args),
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
%Actor{} = creator <- Actors.get_actor(creator_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator},
todo_as_data <-
Convertible.model_to_as(todo),
audience <- %{"to" => [group.members_url], "cc" => []},
create_data <-
make_create_data(todo_as_data, Map.merge(audience, additional)) do
{:ok, todo, create_data}
end
end
@impl Entity
@spec update(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any
def update(%Todo{} = old_todo, args, additional) do
with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args),
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
todo_as_data <-
Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}),
audience <- %{"to" => [group.members_url], "cc" => []},
update_data <-
make_update_data(todo_as_data, Map.merge(audience, additional)) do
{:ok, todo, update_data}
end
end
@impl Entity
@spec delete(Todo.t(), Actor.t(), boolean()) :: {:ok, ActivityStream.t(), Actor.t(), Todo.t()}
def delete(
%Todo{url: url, creator: %Actor{url: group_url}} = todo,
%Actor{url: actor_url} = actor,
_local
) do
Logger.debug("Building Delete Todo activity")
activity_data = %{
"actor" => actor_url,
"type" => "Delete",
"object" => Convertible.model_to_as(url),
"id" => url <> "/delete",
"to" => [group_url]
}
with {:ok, _todo} <- Todos.delete_todo(todo),
{:ok, true} <- Cachex.del(:activity_pub, "todo_#{todo.id}") do
{:ok, activity_data, actor, todo}
end
end
def actor(%Todo{creator_id: creator_id}), do: Actors.get_actor(creator_id)
def group_actor(%Todo{todo_list_id: todo_list_id}) do
case Todos.get_todo_list(todo_list_id) do
%TodoList{actor_id: group_id} ->
Actors.get_actor(group_id)
_ ->
nil
end
end
end

View File

@@ -0,0 +1,14 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
@moduledoc false
alias Mobilizon.{Actors, Tombstone}
alias Mobilizon.Actors.Actor
def actor(%Tombstone{actor: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id)
def actor(%Tombstone{actor_id: actor_id}) when not is_nil(actor_id),
do: Actors.get_actor(actor_id)
def actor(_), do: nil
def group_actor(_), do: nil
end

View File

@@ -8,13 +8,16 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
Various ActivityPub related utils.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Media.Picture
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay}
alias Mobilizon.Federation.ActivityPub.Types.Ownable
alias Mobilizon.Federation.ActivityStream.Converter
alias Mobilizon.Federation.HTTPSignatures
alias Mobilizon.Web.Endpoint
require Logger
@@ -114,6 +117,53 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def maybe_federate(_), do: :ok
@doc """
Applies to activities sent by group members from outside this instance to a group of this instance,
we then need to relay (`Announce`) the object to other members on other instances.
"""
def maybe_relay_if_group_activity(activity, attributed_to \\ nil)
def maybe_relay_if_group_activity(
%Activity{local: false, data: %{"object" => object}},
_attributed_to
)
when is_map(object) do
do_maybe_relay_if_group_activity(object, object["attributedTo"])
end
# When doing a delete the object is just an AP ID string, so we pass the attributed_to actor as well
def maybe_relay_if_group_activity(
%Activity{local: false, data: %{"object" => object}},
%Actor{url: attributed_to_url}
)
when is_binary(object) do
do_maybe_relay_if_group_activity(object, attributed_to_url)
end
def maybe_relay_if_group_activity(_, _), do: :ok
defp do_maybe_relay_if_group_activity(object, attributed_to) when not is_nil(attributed_to) do
id = "#{Endpoint.url()}/announces/#{Ecto.UUID.generate()}"
case Actors.get_local_group_by_url(attributed_to) do
%Actor{} = group ->
case ActivityPub.announce(group, object, id, true, false) do
{:ok, _activity, _object} ->
Logger.info("Forwarded activity to external members of the group")
:ok
_ ->
Logger.info("Failed to forward activity to external members of the group")
:error
end
_ ->
:ok
end
end
defp do_maybe_relay_if_group_activity(_, _), do: :ok
@spec remote_actors(list(String.t())) :: list(Actor.t())
def remote_actors(recipients) do
recipients
@@ -135,7 +185,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
Adds an id and a published data if they aren't there,
also adds it to an included object
"""
def lazy_put_activity_defaults(map) do
def lazy_put_activity_defaults(%{"object" => _object} = map) do
if is_map(map["object"]) do
object = lazy_put_object_defaults(map["object"])
%{map | "object" => object}
@@ -147,7 +197,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Adds an id and published date if they aren't there.
"""
def lazy_put_object_defaults(map) do
def lazy_put_object_defaults(map) when is_map(map) do
Map.put_new_lazy(map, "published", &make_date/0)
end
@@ -175,25 +225,49 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Checks that an incoming AP object's actor matches the domain it came from.
Takes the actor or attributedTo attributes (considers only the first elem if they're an array)
"""
def origin_check?(id, %{"actor" => actor, "attributedTo" => _attributed_to} = params)
when not is_nil(actor) and actor != "" do
params = Map.delete(params, "attributedTo")
origin_check?(id, params)
end
def origin_check?(id, %{"attributedTo" => actor} = params) do
params = params |> Map.put("actor", actor) |> Map.delete("attributedTo")
origin_check?(id, params)
end
def origin_check?(id, %{"actor" => actor} = params) when not is_nil(actor) do
id_uri = URI.parse(id)
actor_uri = URI.parse(get_actor(params))
compare_uris?(actor_uri, id_uri)
def origin_check?(id, %{"actor" => actor} = params)
when not is_nil(actor) and is_list(actor) and length(actor) > 0 do
origin_check?(id, Map.put(params, "actor", hd(actor)))
end
def origin_check?(_id, %{"actor" => nil}), do: false
def origin_check?(id, %{"actor" => actor} = params)
when not is_nil(actor) do
actor = get_actor(params)
Logger.debug("Performing origin check on #{id} and #{actor} URIs")
compare_origins?(id, actor)
end
def origin_check?(_id, _data), do: false
def origin_check?(_id, %{"type" => type} = _params) when type in ["Actor", "Group"], do: true
def origin_check?(_id, %{"actor" => nil} = _args), do: false
def origin_check?(_id, _args), do: false
@spec compare_origins?(String.t(), String.t()) :: boolean()
def compare_origins?(url_1, url_2) when is_binary(url_1) and is_binary(url_2) do
uri_1 = URI.parse(url_1)
uri_2 = URI.parse(url_2)
compare_uris?(uri_1, uri_2)
end
defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri), do: id_uri.host == other_uri.host
@spec origin_check_from_id?(String.t(), String.t()) :: boolean()
def origin_check_from_id?(id, other_id) when is_binary(other_id) do
id_uri = URI.parse(id)
other_uri = URI.parse(other_id)
@@ -201,9 +275,20 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
compare_uris?(id_uri, other_uri)
end
@spec origin_check_from_id?(String.t(), map()) :: boolean()
def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id),
do: origin_check_from_id?(id, other_id)
def activity_actor_is_group_member?(%Actor{id: actor_id}, object) do
case Ownable.group_actor(object) do
%Actor{type: :Group, id: group_id} ->
Actors.is_member?(actor_id, group_id)
_ ->
false
end
end
@doc """
Save picture data from %Plug.Upload{} and return AS Link data.
"""
@@ -274,7 +359,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
activity_id,
public
)
when type in ["Note", "Event", "ResourceCollection", "Document"] do
when type in ["Note", "Event", "ResourceCollection", "Document", "Todo"] do
do_make_announce_data(
actor,
object_actor_url,
@@ -367,6 +452,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
"type" => "Create",
"to" => object["to"],
"cc" => object["cc"],
"attributedTo" => object["attributedTo"] || object["actor"],
"actor" => object["actor"],
"object" => object,
"published" => make_date(),
@@ -494,7 +580,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Sign a request with the instance Relay actor.
"""
@spec sign_fetch_relay(List.t(), String.t(), String.t()) :: List.t()
@spec sign_fetch_relay(Enum.t(), String.t(), String.t()) :: Enum.t()
def sign_fetch_relay(headers, id, date) do
with %Actor{} = actor <- Relay.get_actor() do
sign_fetch(headers, actor, id, date)
@@ -504,7 +590,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Sign a request with an actor.
"""
@spec sign_fetch(List.t(), Actor.t(), String.t(), String.t()) :: List.t()
@spec sign_fetch(Enum.t(), Actor.t(), String.t(), String.t()) :: Enum.t()
def sign_fetch(headers, actor, id, date) do
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do
headers ++ make_signature(actor, id, date)
@@ -516,7 +602,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Add the Date header to the request if we sign object fetches
"""
@spec maybe_date_fetch(List.t(), String.t()) :: List.t()
@spec maybe_date_fetch(Enum.t(), String.t()) :: Enum.t()
def maybe_date_fetch(headers, date) do
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do
headers ++ [{:Date, date}]
@@ -524,4 +610,15 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
headers
end
end
def check_for_actor_key_rotation(%Actor{} = actor) do
if Actors.should_rotate_actor_key(actor) do
Actors.schedule_key_rotation(
actor,
Application.get_env(:mobilizon, :activitypub)[:actor_key_rotation_delay]
)
end
:ok
end
end

View File

@@ -8,7 +8,7 @@ defmodule Mobilizon.Federation.ActivityPub.Visibility do
Utility functions related to content visibility
"""
alias Mobilizon.Conversations.Comment
alias Mobilizon.Discussions.Comment
alias Mobilizon.Federation.ActivityPub.Activity

View File

@@ -0,0 +1,7 @@
defmodule Mobilizon.Federation.ActivityStream do
@moduledoc """
The ActivityStream Type
"""
@type t :: map()
end

View File

@@ -49,7 +49,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
banner: banner,
name: data["name"],
preferred_username: data["preferredUsername"],
summary: data["summary"],
summary: data["summary"] || "",
keys: data["publicKey"]["publicKeyPem"],
inbox_url: data["inbox"],
outbox_url: data["outbox"],
@@ -57,6 +57,10 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
followers_url: data["followers"],
members_url: data["members"],
resources_url: data["resources"],
todos_url: data["todos"],
events_url: data["events"],
posts_url: data["posts"],
discussions_url: data["discussions"],
shared_inbox_url: data["endpoints"]["sharedInbox"],
domain: URI.parse(data["id"]).host,
manually_approves_followers: data["manuallyApprovesFollowers"],
@@ -77,12 +81,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
"type" => actor.type,
"preferredUsername" => actor.preferred_username,
"name" => actor.name,
"summary" => actor.summary,
"summary" => actor.summary || "",
"following" => actor.following_url,
"followers" => actor.followers_url,
"members" => actor.members_url,
"resources" => actor.resources_url,
"todos" => actor.todos_url,
"posts" => actor.posts_url,
"events" => actor.events_url,
"discussions" => actor.discussions_url,
"inbox" => actor.inbox_url,
"outbox" => actor.outbox_url,
"url" => actor.url,

View File

@@ -7,22 +7,30 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Conversations.Comment, as: CommentModel
alias Mobilizon.Discussions
alias Mobilizon.Discussions.Comment, as: CommentModel
alias Mobilizon.Discussions.Discussion
alias Mobilizon.Events.Event
alias Mobilizon.Tombstone, as: TombstoneModel
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Visibility
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Converter.Comment, as: CommentConverter
alias Mobilizon.Tombstone, as: TombstoneModel
import Mobilizon.Federation.ActivityStream.Converter.Utils,
only: [
fetch_tags: 1,
fetch_mentions: 1,
build_tags: 1,
build_mentions: 1,
maybe_fetch_actor_and_attributed_to_id: 1
]
require Logger
@behaviour Converter
defimpl Convertible, for: CommentModel do
alias Mobilizon.Federation.ActivityStream.Converter.Comment, as: CommentConverter
defdelegate model_to_as(comment), to: CommentConverter
end
@@ -35,61 +43,35 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
Logger.debug("We're converting raw ActivityStream data to a comment entity")
Logger.debug(inspect(object))
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
{:ok, %Actor{id: actor_id, domain: domain, suspended: false}} <-
ActivityPub.get_or_fetch_actor_by_url(author_url),
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(Map.get(object, "tag", []))},
with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <-
maybe_fetch_actor_and_attributed_to_id(object),
{:tags, tags} <- {:tags, fetch_tags(Map.get(object, "tag", []))},
{:mentions, mentions} <-
{:mentions, ConverterUtils.fetch_mentions(Map.get(object, "tag", []))} do
{:mentions, fetch_mentions(Map.get(object, "tag", []))},
discussion <-
Discussions.get_discussion_by_url(Map.get(object, "context")) do
Logger.debug("Inserting full comment")
Logger.debug(inspect(object))
data = %{
text: object["content"],
url: object["id"],
# Will be used in conversations, ignored in basic comments
title: object["name"],
context: object["context"],
actor_id: actor_id,
attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id),
in_reply_to_comment_id: nil,
event_id: nil,
uuid: object["uuid"],
discussion_id: if(is_nil(discussion), do: nil, else: discussion.id),
tags: tags,
mentions: mentions,
local: is_nil(domain),
local: is_nil(actor_domain),
visibility: if(Visibility.is_public?(object), do: :public, else: :private)
}
# We fetch the parent object
Logger.debug("We're fetching the parent object")
if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
object["inReplyTo"] != "" do
Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
# Reply to an event (Event)
{:ok, %Event{id: id}} ->
Logger.debug("Parent object is an event")
data |> Map.put(:event_id, id)
# Reply to a comment (Comment)
{:ok, %CommentModel{id: id} = comment} ->
Logger.debug("Parent object is another comment")
data
|> Map.put(:in_reply_to_comment_id, id)
|> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
|> Map.put(:event_id, comment.event_id)
# Anything else is kind of a MP
{:error, parent} ->
Logger.warn("Parent object is something we don't handle")
Logger.debug(inspect(parent))
data
end
else
Logger.debug("No parent object for this comment")
data
end
maybe_fetch_parent_object(object, data)
else
{:ok, %Actor{suspended: true}} ->
:error
@@ -102,10 +84,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
@impl Converter
@spec model_to_as(CommentModel.t()) :: map
def model_to_as(%CommentModel{deleted_at: nil} = comment) do
to =
if comment.visibility == :public,
do: ["https://www.w3.org/ns/activitystreams#Public"],
else: [comment.actor.followers_url]
to = determine_to(comment)
object = %{
"type" => "Note",
@@ -114,13 +93,19 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
"content" => comment.text,
"mediaType" => "text/html",
"actor" => comment.actor.url,
"attributedTo" => comment.actor.url,
"attributedTo" =>
if(is_nil(comment.attributed_to), do: nil, else: comment.attributed_to.url) ||
comment.actor.url,
"uuid" => comment.uuid,
"id" => comment.url,
"tag" =>
ConverterUtils.build_mentions(comment.mentions) ++ ConverterUtils.build_tags(comment.tags)
"tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags)
}
object =
if comment.discussion_id,
do: Map.put(object, "context", comment.discussion.url),
else: object
cond do
comment.in_reply_to_comment ->
Map.put(object, "inReplyTo", comment.in_reply_to_comment.url)
@@ -133,15 +118,78 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
end
end
@impl Converter
@spec model_to_as(CommentModel.t()) :: map
@doc """
A "soft-deleted" comment is a tombstone
"""
@impl Converter
@spec model_to_as(CommentModel.t()) :: map
def model_to_as(%CommentModel{} = comment) do
Convertible.model_to_as(%TombstoneModel{
uri: comment.url,
inserted_at: comment.deleted_at
})
end
@spec determine_to(CommentModel.t()) :: [String.t()]
defp determine_to(%CommentModel{} = comment) do
cond do
not is_nil(comment.attributed_to) ->
[comment.attributed_to.url]
comment.visibility == :public ->
["https://www.w3.org/ns/activitystreams#Public"]
true ->
[comment.actor.followers_url]
end
end
defp maybe_fetch_parent_object(object, data) do
# We fetch the parent object
Logger.debug("We're fetching the parent object")
if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
object["inReplyTo"] != "" do
Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
# Reply to an event (Event)
{:ok, %Event{id: id}} ->
Logger.debug("Parent object is an event")
data |> Map.put(:event_id, id)
# Reply to a comment (Comment)
{:ok, %CommentModel{id: id} = comment} ->
Logger.debug("Parent object is another comment")
data
|> Map.put(:in_reply_to_comment_id, id)
|> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
|> Map.put(:event_id, comment.event_id)
# Reply to a discucssion (Discussion)
{:ok,
%Discussion{
id: discussion_id,
last_comment: %CommentModel{id: last_comment_id, origin_comment_id: origin_comment_id}
} = _discussion} ->
Logger.debug("Parent object is a discussion")
data
|> Map.put(:in_reply_to_comment_id, last_comment_id)
|> Map.put(:origin_comment_id, origin_comment_id)
|> Map.put(:discussion_id, discussion_id)
# Anything else is kind of a MP
{:error, parent} ->
Logger.warn("Parent object is something we don't handle")
Logger.debug(inspect(parent))
data
end
else
Logger.debug("No parent object for this comment")
data
end
end
end

View File

@@ -6,6 +6,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter do
one, and back.
"""
@callback as_to_model_data(map) :: map
@callback model_to_as(struct) :: map
@type model_data :: map()
@callback as_to_model_data(as_data :: ActivityStream.t()) :: model_data()
@callback model_to_as(model :: struct()) :: ActivityStream.t()
end

View File

@@ -0,0 +1,63 @@
defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
@moduledoc """
Comment converter.
This module allows to convert events from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.Discussion
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.Converter.Discussion, as: DiscussionConverter
alias Mobilizon.Storage.Repo
require Logger
@behaviour Converter
defimpl Convertible, for: Discussion do
defdelegate model_to_as(comment), to: DiscussionConverter
end
@doc """
Make an AS comment object from an existing `discussion` structure.
"""
@impl Converter
@spec model_to_as(Discussion.t()) :: map
def model_to_as(%Discussion{} = discussion) do
discussion = Repo.preload(discussion, [:last_comment, :actor, :creator])
%{
"type" => "Note",
"to" => [discussion.actor.followers_url],
"cc" => [],
"name" => discussion.title,
"content" => discussion.last_comment.text,
"mediaType" => "text/html",
"actor" => discussion.creator.url,
"attributedTo" => discussion.actor.url,
"id" => discussion.url,
"context" => discussion.url
}
end
@impl Converter
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
def as_to_model_data(%{"type" => "Note", "name" => name} = object) when not is_nil(name) do
with creator_url <- Map.get(object, "actor"),
{:ok, %Actor{id: creator_id, suspended: false}} <-
ActivityPub.get_or_fetch_actor_by_url(creator_url),
actor_url <- Map.get(object, "attributedTo"),
{:ok, %Actor{id: actor_id, suspended: false}} <-
ActivityPub.get_or_fetch_actor_by_url(actor_url) do
%{
title: name,
actor_id: actor_id,
creator_id: creator_id,
url: object["id"]
}
end
end
end

View File

@@ -12,11 +12,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Media.Picture
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter
alias Mobilizon.Federation.ActivityStream.Converter.Picture, as: PictureConverter
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
import Mobilizon.Federation.ActivityStream.Converter.Utils,
only: [
fetch_tags: 1,
fetch_mentions: 1,
build_tags: 1,
maybe_fetch_actor_and_attributed_to_id: 1
]
require Logger
@@ -34,16 +40,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
@impl Converter
@spec as_to_model_data(map) :: {:ok, map()} | {:error, any()}
def as_to_model_data(object) do
Logger.debug("event as_to_model_data")
Logger.debug(inspect(object))
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
{:actor, {:ok, %Actor{id: actor_id, domain: actor_domain, suspended: false}}} <-
{:actor, ActivityPub.get_or_fetch_actor_by_url(author_url)},
with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <-
maybe_fetch_actor_and_attributed_to_id(object),
{:address, address_id} <-
{:address, get_address(object["location"])},
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])},
{:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(object["tag"])},
{:tags, tags} <- {:tags, fetch_tags(object["tag"])},
{:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])},
{:visibility, visibility} <- {:visibility, get_visibility(object)},
{:options, options} <- {:options, get_options(object)} do
attachments =
@@ -67,6 +69,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
title: object["name"],
description: object["content"],
organizer_actor_id: actor_id,
attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id),
picture_id: picture_id,
begins_on: object["startTime"],
ends_on: object["endTime"],
@@ -108,7 +111,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
"type" => "Event",
"to" => to,
"cc" => [],
"attributedTo" => event.organizer_actor.url,
"attributedTo" =>
if(is_nil(event.attributed_to), do: nil, else: event.attributed_to.url) ||
event.organizer_actor.url,
"name" => event.title,
"actor" => event.organizer_actor.url,
"uuid" => event.uuid,
@@ -120,7 +125,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
"startTime" => event.begins_on |> date_to_string(),
"joinMode" => to_string(event.join_options),
"endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> ConverterUtils.build_tags(),
"tag" => event.tags |> build_tags(),
"maximumAttendeeCapacity" => event.options.maximum_attendee_capacity,
"repliesModerationOption" => event.options.comment_moderation,
"commentsEnabled" => event.options.comment_moderation == :allow_all,

View File

@@ -9,7 +9,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Conversations
alias Mobilizon.Discussions
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Reports.Report
@@ -92,7 +92,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do
Enum.filter(objects, fn url ->
!(url == reported.url || (!is_nil(event) && event.url == url))
end),
comments <- Enum.map(comments, &Conversations.get_comment_from_url/1) do
comments <- Enum.map(comments, &Discussions.get_comment_from_url/1) do
%{
"reporter" => reporter,
"uri" => object["id"],

View File

@@ -39,7 +39,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Picture do
actor_id
)
when is_bitstring(picture_url) do
with {:ok, %HTTPoison.Response{body: body}} <- HTTPoison.get(picture_url, [], @http_options),
with {:ok, %{body: body}} <- Tesla.get(picture_url, opts: @http_options),
{:ok, %{name: name, url: url, content_type: content_type, size: size}} <-
Upload.store(%{body: body, name: name}),
{:picture_exists, nil} <- {:picture_exists, Media.get_picture_by_url(url)} do

View File

@@ -0,0 +1,70 @@
defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
@moduledoc """
Post converter.
This module allows to convert posts from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Utils
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Posts.Post
require Logger
@behaviour Converter
defimpl Convertible, for: Post do
alias Mobilizon.Federation.ActivityStream.Converter.Post, as: PostConverter
defdelegate model_to_as(post), to: PostConverter
end
@doc """
Convert an post struct to an ActivityStream representation
"""
@impl Converter
@spec model_to_as(Post.t()) :: map
def model_to_as(
%Post{author: %Actor{url: actor_url}, attributed_to: %Actor{url: creator_url}} = post
) do
%{
"type" => "Article",
"actor" => actor_url,
"id" => post.url,
"name" => post.title,
"content" => post.body,
"attributedTo" => creator_url,
"published" => post.publish_at || post.inserted_at
}
end
@doc """
Converts an AP object data to our internal data structure.
"""
@impl Converter
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
def as_to_model_data(
%{"type" => "Article", "actor" => creator, "attributedTo" => group} = object
) do
with {:ok, %Actor{id: attributed_to_id}} <- get_actor(group),
{:ok, %Actor{id: author_id}} <- get_actor(creator) do
%{
title: object["name"],
body: object["content"],
url: object["id"],
attributed_to_id: attributed_to_id,
author_id: author_id,
local: false,
publish_at: object["published"]
}
else
{:error, err} -> {:error, err}
err -> {:error, err}
end
end
@spec get_actor(String.t() | map() | nil) :: {:ok, Actor.t()} | {:error, String.t()}
defp get_actor(nil), do: {:error, "nil property found for actor data"}
defp get_actor(actor), do: actor |> Utils.get_url() |> ActivityPub.get_or_fetch_actor_by_url()
end

View File

@@ -28,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do
"type" => "TodoList",
"actor" => group_url,
"id" => todo_list.url,
"title" => todo_list.title
"name" => todo_list.title
}
end

View File

@@ -28,6 +28,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Tombstone do
%{
"type" => "Tombstone",
"id" => tombstone.uri,
"actor" => tombstone.actor.url,
"deleted" => tombstone.inserted_at
}
end

View File

@@ -23,6 +23,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
tags |> Enum.flat_map(&fetch_tag/1) |> Enum.uniq() |> Enum.map(&existing_tag_or_data/1)
end
def fetch_tags(_), do: []
@spec fetch_mentions([map()]) :: [map()]
def fetch_mentions(mentions) when is_list(mentions) do
Logger.debug("fetching mentions")
@@ -30,6 +32,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
Enum.reduce(mentions, [], fn mention, acc -> create_mention(mention, acc) end)
end
def fetch_mentions(_), do: []
def fetch_address(%{id: id}) do
with {id, ""} <- Integer.parse(id), do: %{id: id}
end
@@ -38,7 +42,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
address
end
@spec build_tags([Tag.t()]) :: [Map.t()]
@spec build_tags([Tag.t()]) :: [map()]
def build_tags(tags) do
Enum.map(tags, fn %Tag{} = tag ->
%{
@@ -111,4 +115,51 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
defp create_mention({_, mention}, acc) when is_map(mention) do
create_mention(mention, acc)
end
@spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
def maybe_fetch_actor_and_attributed_to_id(%{
"actor" => actor_url,
"attributedTo" => attributed_to_url
})
when is_nil(attributed_to_url) do
{fetch_actor(actor_url), nil}
end
@spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
def maybe_fetch_actor_and_attributed_to_id(%{
"actor" => actor_url,
"attributedTo" => attributed_to_url
})
when is_nil(actor_url) do
{fetch_actor(attributed_to_url), nil}
end
# Only when both actor and attributedTo fields are both filled is when we can return both
def maybe_fetch_actor_and_attributed_to_id(%{
"actor" => actor_url,
"attributedTo" => attributed_to_url
})
when actor_url != attributed_to_url do
with actor <- fetch_actor(actor_url),
attributed_to <- fetch_actor(attributed_to_url) do
{actor, attributed_to}
end
end
# If we only have attributedTo and no actor, take attributedTo as the actor
def maybe_fetch_actor_and_attributed_to_id(%{
"attributedTo" => attributed_to_url
}) do
{fetch_actor(attributed_to_url), nil}
end
def maybe_fetch_actor_and_attributed_to_id(_), do: {nil, nil}
@spec fetch_actor(String.t()) :: Actor.t()
defp fetch_actor(actor_url) do
with {:ok, %Actor{suspended: false} = actor} <-
ActivityPub.get_or_fetch_actor_by_url(actor_url) do
actor
end
end
end

View File

@@ -3,8 +3,9 @@ defprotocol Mobilizon.Federation.ActivityStream.Convertible do
Convertible protocol.
"""
@type activity_streams :: map
@type t :: struct()
@type activity_streams :: map()
@spec model_to_as(t) :: activity_streams
@spec model_to_as(t()) :: activity_streams()
def model_to_as(convertible)
end

View File

@@ -118,13 +118,15 @@ defmodule Mobilizon.Federation.WebFinger do
Logger.debug(inspect(address))
with false <- is_nil(domain),
{:ok, %HTTPoison.Response{} = response} <-
HTTPoison.get(
{:ok, %{} = response} <-
Tesla.get(
address,
[Accept: "application/json, application/activity+json, application/jrd+json"],
@http_options
headers: [
{"accept", "application/json, application/activity+json, application/jrd+json"}
],
opts: @http_options
),
%{status_code: status_code, body: body} when status_code in 200..299 <- response,
%{status: status, body: body} when status in 200..299 <- response,
{:ok, doc} <- Jason.decode(body) do
webfinger_from_json(doc)
else