Add admin interface to manage instances subscriptions

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-12-03 11:29:51 +01:00
parent 0a96d70348
commit 334d66bf5d
141 changed files with 4198 additions and 1923 deletions

View File

@@ -11,7 +11,7 @@ defmodule Mobilizon.Service.ActivityPub do
import Mobilizon.Service.ActivityPub.Utils
import Mobilizon.Service.ActivityPub.Visibility
alias Mobilizon.{Actors, Config, Events, Reports, Users}
alias Mobilizon.{Actors, Config, Events, Reports, Users, Share}
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Events.{Comment, Event, Participant}
alias Mobilizon.Reports.Report
@@ -50,6 +50,15 @@ defmodule Mobilizon.Service.ActivityPub do
def fetch_object_from_url(url) do
Logger.info("Fetching object from url #{url}")
date = Mobilizon.Service.HTTPSignatures.Signature.generate_date_header()
headers =
[{:Accept, "application/activity+json"}]
|> maybe_date_fetch(date)
|> sign_fetch(url, date)
Logger.debug("Fetch headers: #{inspect(headers)}")
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, Events.get_comment_from_url(url)},
@@ -58,12 +67,13 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
HTTPoison.get(
url,
[Accept: "application/activity+json"],
headers,
follow_redirect: true,
timeout: 10_000,
recv_timeout: 20_000
),
{:ok, data} <- Jason.decode(body),
{:origin_check, true} <- {:origin_check, origin_check?(url, data)},
params <- %{
"type" => "Create",
"to" => data["to"],
@@ -95,6 +105,10 @@ defmodule Mobilizon.Service.ActivityPub do
{:existing_actor, {:ok, %Actor{url: actor_url}}} ->
{:ok, Actors.get_actor_by_url!(actor_url, true)}
{:origin_check, false} ->
Logger.warn("Object origin check failed")
{:error, "Object origin check failed"}
e ->
{:error, e}
end
@@ -114,9 +128,9 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, %Actor{} = actor} ->
{:ok, actor}
_ ->
err ->
Logger.warn("Could not fetch by AP id")
Logger.debug(inspect(err))
{:error, "Could not fetch by AP id"}
end
end
@@ -184,11 +198,13 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
def accept(type, entity, args, local \\ false, additional \\ %{}) do
def accept(type, entity, local \\ true, additional \\ %{}) do
Logger.debug("We're accepting something")
{:ok, entity, update_data} =
case type do
:join -> accept_join(entity, args, additional)
:follow -> accept_follow(entity, args, additional)
:join -> accept_join(entity, additional)
:follow -> accept_follow(entity, additional)
end
with {:ok, activity} <- create_activity(update_data, local),
@@ -202,63 +218,24 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
def reject(%{to: to, actor: actor, object: object} = params, activity_wrapper_id \\ nil) do
# only accept false as false value
local = !(params[:local] == false)
def reject(type, entity, local \\ true, additional \\ %{}) do
{:ok, entity, update_data} =
case type do
:join -> reject_join(entity, additional)
:follow -> reject_follow(entity, additional)
end
with data <- %{
"to" => to,
"type" => "Reject",
"actor" => actor,
"object" => object,
"id" => activity_wrapper_id || get_url(object) <> "/activity"
},
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
with {:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
{:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
end
end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
# def like(
# %Actor{url: url} = actor,
# object,
# activity_id \\ nil,
# local \\ true
# ) do
# with nil <- get_existing_like(url, object),
# like_data <- make_like_data(user, object, activity_id),
# {:ok, activity} <- create_activity(like_data, local),
# {:ok, object} <- insert_full_object(data),
# {:ok, object} <- add_like_to_object(activity, object),
# :ok <- maybe_federate(activity) do
# {:ok, activity, object}
# else
# %Activity{} = activity -> {:ok, activity, object}
# error -> {:error, error}
# end
# end
# def unlike(
# %User{} = actor,
# %Object{} = object,
# activity_id \\ nil,
# local \\ true
# ) do
# with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
# unlike_data <- make_unlike_data(actor, like_activity, activity_id),
# {:ok, unlike_activity} <- create_activity(unlike_data, local),
# {:ok, _object} <- insert_full_object(data),
# {:ok, _activity} <- Repo.delete(like_activity),
# {:ok, object} <- remove_like_from_object(like_activity, object),
# :ok <- maybe_federate(unlike_activity) do
# {:ok, unlike_activity, like_activity, object}
# else
# _e -> {:ok, object}
# end
# end
def announce(
%Actor{} = actor,
object,
@@ -267,9 +244,10 @@ defmodule Mobilizon.Service.ActivityPub do
public \\ true
) do
with true <- is_public?(object),
{:ok, %Actor{id: object_owner_actor_id}} <- Actors.get_actor_by_url(object["actor"]),
{:ok, %Share{} = _share} <- Share.create(object["id"], actor.id, object_owner_actor_id),
announce_data <- make_announce_data(actor, object, activity_id, public),
{:ok, activity} <- create_activity(announce_data, local),
{:ok, object} <- insert_full_object(announce_data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
@@ -288,7 +266,6 @@ defmodule Mobilizon.Service.ActivityPub do
with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- create_activity(unannounce_data, local),
{:ok, object} <- insert_full_object(unannounce_data),
:ok <- maybe_federate(unannounce_activity) do
{:ok, unannounce_activity, object}
else
@@ -327,9 +304,8 @@ defmodule Mobilizon.Service.ActivityPub do
unfollow_data <-
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id),
{:ok, activity} <- create_activity(unfollow_data, local),
{:ok, object} <- insert_full_object(unfollow_data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
{:ok, activity, follow}
else
err ->
Logger.debug("Error while unfollowing an actor #{inspect(err)}")
@@ -339,6 +315,7 @@ defmodule Mobilizon.Service.ActivityPub do
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",
@@ -348,15 +325,19 @@ defmodule Mobilizon.Service.ActivityPub do
"id" => url <> "/delete"
}
with {:ok, %Event{} = event} <- Events.delete_event(event),
with audience <-
Audience.calculate_to_and_cc_from_mentions(event),
{:ok, %Event{} = event} <- Events.delete_event(event),
{:ok, %Tombstone{} = _tombstone} <-
Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}),
{:ok, activity} <- create_activity(data, local),
Share.delete_all_by_uri(event.url),
{:ok, activity} <- create_activity(Map.merge(data, audience), local),
:ok <- maybe_federate(activity) do
{:ok, activity, event}
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",
@@ -366,10 +347,13 @@ defmodule Mobilizon.Service.ActivityPub do
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
}
with {:ok, %Comment{} = comment} <- Events.delete_comment(comment),
with audience <-
Audience.calculate_to_and_cc_from_mentions(comment),
{:ok, %Comment{} = comment} <- Events.delete_comment(comment),
{:ok, %Tombstone{} = _tombstone} <-
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}),
{:ok, activity} <- create_activity(data, local),
Share.delete_all_by_uri(comment.url),
{:ok, activity} <- create_activity(Map.merge(data, audience), local),
:ok <- maybe_federate(activity) do
{:ok, activity, comment}
end
@@ -384,7 +368,7 @@ defmodule Mobilizon.Service.ActivityPub do
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
}
with {:ok, %Actor{} = actor} <- Actors.delete_actor(actor),
with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor),
{:ok, activity} <- create_activity(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, actor}
@@ -396,6 +380,8 @@ defmodule Mobilizon.Service.ActivityPub do
{: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 ->
@@ -413,52 +399,56 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
def join(object, actor, local \\ true)
def join(object, actor, local \\ true, additional \\ %{})
def join(%Event{options: options} = event, %Actor{} = actor, local) do
def join(%Event{} = event, %Actor{} = actor, local, additional) do
# TODO Refactor me for federation
with maximum_attendee_capacity <-
Map.get(options, :maximum_attendee_capacity) || 0,
{:maximum_attendee_capacity, true} <-
{:maximum_attendee_capacity,
maximum_attendee_capacity == 0 ||
Mobilizon.Events.count_participant_participants(event.id) <
maximum_attendee_capacity},
role <- Mobilizon.Events.get_default_participant_role(event),
with {:maximum_attendee_capacity, true} <-
{:maximum_attendee_capacity, check_attendee_capacity(event)},
{:ok, %Participant{} = participant} <-
Mobilizon.Events.create_participant(%{
role: role,
role: :not_approved,
event_id: event.id,
actor_id: actor.id
actor_id: actor.id,
url: Map.get(additional, :url)
}),
join_data <- Convertible.model_to_as(participant),
join_data <- Map.put(join_data, "to", [event.organizer_actor.url]),
join_data <- Map.put(join_data, "cc", []),
{:ok, activity} <- create_activity(join_data, local),
{:ok, _object} <- insert_full_object(join_data),
audience <-
Audience.calculate_to_and_cc_from_mentions(participant),
{:ok, activity} <- create_activity(Map.merge(join_data, audience), local),
:ok <- maybe_federate(activity) do
if role === :participant do
accept_join(
if event.local && Mobilizon.Events.get_default_participant_role(event) === :participant do
accept(
:join,
participant,
%{}
true,
%{"actor" => event.organizer_actor.url}
)
else
{:ok, activity, participant}
end
{:ok, activity, participant}
end
end
# TODO: Implement me
def join(%Actor{type: :Group} = _group, %Actor{} = _actor, _local) do
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
end
end
def leave(object, actor, local \\ true)
# TODO: If we want to use this for exclusion we need to have an extra field
# for the actor that excluded the participant
def leave(
%Event{id: event_id, url: event_url} = event,
%Event{id: event_id, url: event_url} = _event,
%Actor{id: actor_id, url: actor_url} = _actor,
local
) do
@@ -473,11 +463,11 @@ defmodule Mobilizon.Service.ActivityPub do
# If it's an exclusion it should be something else
"actor" => actor_url,
"object" => event_url,
"to" => [event.organizer_actor.url],
"cc" => []
"id" => "#{MobilizonWeb.Endpoint.url()}/leave/event/#{participant.id}"
},
{:ok, activity} <- create_activity(leave_data, local),
{:ok, _object} <- insert_full_object(leave_data),
audience <-
Audience.calculate_to_and_cc_from_mentions(participant),
{:ok, activity} <- create_activity(Map.merge(leave_data, audience), local),
:ok <- maybe_federate(activity) do
{:ok, activity, participant}
end
@@ -537,16 +527,22 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
@spec is_create_activity?(Activity.t()) :: boolean
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
defp is_create_activity?(_), do: false
@doc """
Publish an activity to all appropriated audiences inboxes
"""
@spec publish(Actor.t(), Activity.t()) :: :ok
def publish(actor, activity) do
Logger.debug("Publishing an activity")
Logger.debug(inspect(activity))
public = is_public?(activity)
Logger.debug("is public ? #{public}")
if public && !is_delete_activity?(activity) && Config.get([:instance, :allow_relay]) do
if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
@@ -578,15 +574,12 @@ defmodule Mobilizon.Service.ActivityPub do
end)
end
defp is_delete_activity?(%Activity{data: %{"type" => "Delete"}}), do: true
defp is_delete_activity?(_), do: false
@doc """
Publish an activity to a specific inbox
"""
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}")
%URI{host: host, path: _path} = URI.parse(inbox)
%URI{host: host, path: path} = URI.parse(inbox)
digest = Signature.build_digest(json)
date = Signature.generate_date_header()
@@ -594,10 +587,9 @@ defmodule Mobilizon.Service.ActivityPub do
signature =
Signature.sign(actor, %{
"(request-target)": "post #{path}",
host: host,
"content-length": byte_size(json),
# TODO : Look me up in depth why Pleroma handles this inside lib/mobilizon_web/http_signature.ex
# "(request-target)": request_target,
digest: digest,
date: date
})
@@ -627,7 +619,7 @@ defmodule Mobilizon.Service.ActivityPub do
: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")
actor_data_from_actor_object(data)
Mobilizon.Service.ActivityPub.Converter.Actor.as_to_model_data(data)
else
# Actor is gone, probably deleted
{:ok, %HTTPoison.Response{status_code: 410}} ->
@@ -642,49 +634,6 @@ defmodule Mobilizon.Service.ActivityPub do
res
end
@doc """
Creating proper actor data struct from AP data
Convert ActivityPub data to our internal format
"""
@spec actor_data_from_actor_object(map()) :: {:ok, map()}
def actor_data_from_actor_object(data) when is_map(data) do
avatar =
data["icon"]["url"] &&
%{
"name" => data["icon"]["name"] || "avatar",
"url" => data["icon"]["url"]
}
banner =
data["image"]["url"] &&
%{
"name" => data["image"]["name"] || "banner",
"url" => data["image"]["url"]
}
actor_data = %{
url: data["id"],
avatar: avatar,
banner: banner,
name: data["name"],
preferred_username: data["preferredUsername"],
summary: data["summary"],
keys: data["publicKey"]["publicKeyPem"],
inbox_url: data["inbox"],
outbox_url: data["outbox"],
following_url: data["following"],
followers_url: data["followers"],
shared_inbox_url: data["endpoints"]["sharedInbox"],
domain: URI.parse(data["id"]).host,
manually_approves_followers: data["manuallyApprovesFollowers"],
type: data["type"]
}
{:ok, actor_data}
end
@doc """
Return all public activities (events & comments) for an actor
"""
@@ -736,12 +685,7 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, %Event{} = event} <- Events.create_event(args),
event_as_data <- Convertible.model_to_as(event),
audience <-
Audience.calculate_to_and_cc_from_mentions(
event.organizer_actor,
args.mentions,
nil,
event.visibility
),
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}
@@ -754,12 +698,7 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, %Comment{} = comment} <- Events.create_comment(args),
comment_as_data <- Convertible.model_to_as(comment),
audience <-
Audience.calculate_to_and_cc_from_mentions(
comment.actor,
args.mentions,
args.in_reply_to_comment,
comment.visibility
),
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}
@@ -771,13 +710,7 @@ defmodule Mobilizon.Service.ActivityPub 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 <-
Audience.calculate_to_and_cc_from_mentions(
args.creator_actor,
[],
nil,
:public
),
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}
@@ -799,12 +732,7 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, %Event{} = new_event} <- Events.update_event(old_event, args),
event_as_data <- Convertible.model_to_as(new_event),
audience <-
Audience.calculate_to_and_cc_from_mentions(
new_event.organizer_actor,
Map.get(args, :mentions, []),
nil,
new_event.visibility
),
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
@@ -821,34 +749,29 @@ defmodule Mobilizon.Service.ActivityPub do
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
actor_as_data <- Convertible.model_to_as(new_actor),
audience <-
Audience.calculate_to_and_cc_from_mentions(
new_actor,
[],
nil,
:public
),
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 accept_follow(Follower.t(), map(), map()) ::
@spec accept_follow(Follower.t(), map()) ::
{:ok, Follower.t(), Activity.t()} | any()
defp accept_follow(
%Follower{} = follower,
args,
additional
) do
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, args),
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}),
follower_as_data <- Convertible.model_to_as(follower),
audience <-
Audience.calculate_to_and_cc_from_mentions(follower.target_actor),
update_data <-
make_update_data(
make_accept_join_data(
follower_as_data,
Map.merge(Map.merge(audience, additional), %{
"id" => "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follower.id}"
Map.merge(additional, %{
"id" => "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follower.id}",
"to" => [follower.actor.url],
"cc" => [],
"actor" => follower.target_actor.url
})
) do
{:ok, follower, update_data}
@@ -860,17 +783,20 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
@spec accept_join(Participant.t(), map(), map()) ::
@spec accept_join(Participant.t(), map()) ::
{:ok, Participant.t(), Activity.t()} | any()
defp accept_join(
%Participant{} = participant,
args,
additional \\ %{}
additional
) do
with {:ok, %Participant{} = participant} <- Events.update_participant(participant, args),
with {:ok, %Participant{} = participant} <-
Events.update_participant(participant, %{role: :participant}),
Absinthe.Subscription.publish(MobilizonWeb.Endpoint, participant.actor,
event_person_participation_changed: participant.actor.id
),
participant_as_data <- Convertible.model_to_as(participant),
audience <-
Audience.calculate_to_and_cc_from_mentions(participant.actor),
Audience.calculate_to_and_cc_from_mentions(participant),
update_data <-
make_accept_join_data(
participant_as_data,
@@ -887,6 +813,66 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
@spec reject_join(Participant.t(), map()) ::
{:ok, Participant.t(), Activity.t()} | any()
defp reject_join(%Participant{} = participant, additional) do
with {:ok, %Participant{} = participant} <-
Events.update_participant(participant, %{approved: false, role: :rejected}),
Absinthe.Subscription.publish(MobilizonWeb.Endpoint, participant.actor,
event_person_participation_changed: participant.actor.id
),
participant_as_data <- Convertible.model_to_as(participant),
audience <-
participant
|> Audience.calculate_to_and_cc_from_mentions()
|> Map.merge(additional),
reject_data <- %{
"type" => "Reject",
"object" => participant_as_data
},
update_data <-
reject_data
|> Map.merge(audience)
|> Map.merge(%{
"id" => "#{MobilizonWeb.Endpoint.url()}/reject/join/#{participant.id}"
}) do
{:ok, participant, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
@spec reject_follow(Follower.t(), map()) ::
{:ok, Follower.t(), Activity.t()} | any()
defp reject_follow(%Follower{} = follower, additional) do
with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower),
follower_as_data <- Convertible.model_to_as(follower),
audience <-
follower.actor |> Audience.calculate_to_and_cc_from_mentions() |> Map.merge(additional),
reject_data <- %{
"to" => follower.actor.url,
"type" => "Reject",
"actor" => follower.actor.url,
"object" => follower_as_data
},
update_data <-
reject_data
|> Map.merge(audience)
|> Map.merge(%{
"id" => "#{MobilizonWeb.Endpoint.url()}/reject/follow/#{follower.id}"
}) do
{:ok, follower, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
# Prepare and sanitize arguments for events
defp prepare_args_for_event(args) do
# If title is not set: we are not updating it
@@ -923,7 +909,8 @@ defmodule Mobilizon.Service.ActivityPub do
# 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) |> Events.get_comment(),
args |> Map.get(:in_reply_to_comment_id) |> Events.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(
@@ -940,6 +927,7 @@ defmodule Mobilizon.Service.ActivityPub do
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)),
@@ -953,6 +941,16 @@ defmodule Mobilizon.Service.ActivityPub do
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) |> HtmlSanitizeEx.strip_tags() |> String.trim(),

View File

@@ -2,7 +2,13 @@ defmodule Mobilizon.Service.ActivityPub.Audience do
@moduledoc """
Tools for calculating content audience
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Events.Participant
alias Mobilizon.Share
require Logger
@ap_public "https://www.w3.org/ns/activitystreams#Public"
@@ -13,35 +19,27 @@ defmodule Mobilizon.Service.ActivityPub.Audience do
* `to` : the mentioned actors, the eventual actor we're replying to and the public
* `cc` : the actor's followers
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :public) do
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, :public) do
to = [@ap_public | mentions]
cc = [actor.followers_url]
if in_reply_to do
{Enum.uniq([in_reply_to.actor | to]), cc}
else
{to, cc}
end
{to, cc}
end
@doc """
Determines the full audience based on mentions based on a unlisted audience
Audience is:
* `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
* `to` : the mentioned actors, actor's followers and the eventual actor we're replying to
* `cc` : public
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :unlisted) do
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do
to = [actor.followers_url | mentions]
cc = [@ap_public]
if in_reply_to do
{Enum.uniq([in_reply_to.actor | to]), cc}
else
{to, cc}
end
{to, cc}
end
@doc """
@@ -51,9 +49,9 @@ defmodule Mobilizon.Service.ActivityPub.Audience do
* `to` : the mentioned actors, actor's followers and the eventual actor we're replying to
* `cc` : none
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :private) do
{to, cc} = get_to_and_cc(actor, mentions, in_reply_to, :direct)
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, :private) do
{to, cc} = get_to_and_cc(actor, mentions, :direct)
{[actor.followers_url | to], cc}
end
@@ -64,16 +62,12 @@ defmodule Mobilizon.Service.ActivityPub.Audience do
* `to` : the mentioned actors and the eventual actor we're replying to
* `cc` : none
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(_actor, mentions, in_reply_to, :direct) do
if in_reply_to do
{Enum.uniq([in_reply_to.actor | mentions]), []}
else
{mentions, []}
end
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(_actor, mentions, :direct) do
{mentions, []}
end
def get_to_and_cc(_actor, mentions, _in_reply_to, {:list, _}) do
def get_to_and_cc(_actor, mentions, {:list, _}) do
{mentions, []}
end
@@ -83,16 +77,109 @@ defmodule Mobilizon.Service.ActivityPub.Audience do
def get_addressed_actors(mentioned_users, _), do: mentioned_users
def calculate_to_and_cc_from_mentions(
actor,
mentions \\ [],
in_reply_to \\ nil,
visibility \\ :public
) do
with mentioned_actors <- for({_, mentioned_actor} <- mentions, do: mentioned_actor.url),
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),
{to, cc} <- get_to_and_cc(actor, addressed_actors, in_reply_to, visibility) do
{to, cc} <- get_to_and_cc(comment.actor, addressed_actors, comment.visibility),
{to, cc} <- {Enum.uniq(to ++ add_in_reply_to(comment.in_reply_to_comment)), cc},
{to, cc} <- {Enum.uniq(to ++ add_event_author(comment.event)), cc},
{to, cc} <-
{to,
Enum.uniq(
cc ++
add_comments_authors([comment.origin_comment]) ++
add_shares_actors_followers(comment.url)
)} do
%{"to" => to, "cc" => cc}
end
end
def calculate_to_and_cc_from_mentions(%Event{} = event) do
with mentioned_actors <- Enum.map(event.mentions, &process_mention/1),
addressed_actors <- get_addressed_actors(mentioned_actors, nil),
{to, cc} <- get_to_and_cc(event.organizer_actor, addressed_actors, event.visibility),
{to, cc} <-
{to,
Enum.uniq(
cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url)
)} do
%{"to" => to, "cc" => cc}
end
end
def calculate_to_and_cc_from_mentions(%Participant{} = participant) do
participant = Mobilizon.Storage.Repo.preload(participant, [:actor, :event])
actor_participants_urls =
participant.event.id
|> Mobilizon.Events.list_actors_participants_for_event()
|> Enum.map(& &1.url)
%{"to" => [participant.actor.url], "cc" => actor_participants_urls}
end
def calculate_to_and_cc_from_mentions(%Actor{} = actor) do
%{
"to" => [@ap_public],
"cc" => [actor.followers_url] ++ add_actors_that_had_our_content(actor.id)
}
end
defp add_in_reply_to(%Comment{actor: %Actor{url: url}} = _comment), do: [url]
defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url]
defp add_in_reply_to(_), do: []
defp add_event_author(nil), do: []
defp add_event_author(%Event{} = event) do
[Mobilizon.Storage.Repo.preload(event, [:organizer_actor]).organizer_actor.url]
end
defp add_comment_author(nil), do: nil
defp add_comment_author(%Comment{} = comment) do
case Mobilizon.Storage.Repo.preload(comment, [:actor]) do
%Comment{actor: %Actor{url: url}} ->
url
_err ->
nil
end
end
defp add_comments_authors(comments) do
authors =
comments
|> Enum.map(&add_comment_author/1)
|> Enum.filter(& &1)
authors
end
@spec add_shares_actors_followers(String.t()) :: list(String.t())
defp add_shares_actors_followers(uri) do
uri
|> Share.get_actors_by_share_uri()
|> Enum.map(&Actors.list_followers_actors_for_actor/1)
|> List.flatten()
|> Enum.map(& &1.url)
|> Enum.uniq()
end
defp add_actors_that_had_our_content(actor_id) do
actor_id
|> Share.get_actors_by_owner_actor_id()
|> Enum.map(&Actors.list_followers_actors_for_actor/1)
|> List.flatten()
|> Enum.map(& &1.url)
|> Enum.uniq()
end
defp process_mention({_, mentioned_actor}), do: mentioned_actor.url
defp process_mention(%{actor_id: actor_id}) do
with %Actor{url: url} <- Actors.get_actor(actor_id) do
url
end
end
end

View File

@@ -7,7 +7,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Actor do
"""
alias Mobilizon.Actors.Actor, as: ActorModel
alias Mobilizon.Service.ActivityPub.{Converter, Convertible}
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils}
@behaviour Converter
@@ -22,33 +22,40 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Actor do
"""
@impl Converter
@spec as_to_model_data(map) :: map
def as_to_model_data(object) do
def as_to_model_data(data) do
avatar =
object["icon"]["url"] &&
data["icon"]["url"] &&
%{
"name" => object["icon"]["name"] || "avatar",
"url" => object["icon"]["url"]
"name" => data["icon"]["name"] || "avatar",
"url" => MobilizonWeb.MediaProxy.url(data["icon"]["url"])
}
banner =
object["image"]["url"] &&
data["image"]["url"] &&
%{
"name" => object["image"]["name"] || "banner",
"url" => object["image"]["url"]
"name" => data["image"]["name"] || "banner",
"url" => MobilizonWeb.MediaProxy.url(data["image"]["url"])
}
{:ok,
%{
"type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferredUsername"],
"summary" => object["summary"],
"url" => object["id"],
"name" => object["name"],
"avatar" => avatar,
"banner" => banner,
"keys" => object["publicKey"]["publicKeyPem"],
"manually_approves_followers" => object["manuallyApprovesFollowers"]
}}
actor_data = %{
url: data["id"],
avatar: avatar,
banner: banner,
name: data["name"],
preferred_username: data["preferredUsername"],
summary: data["summary"],
keys: data["publicKey"]["publicKeyPem"],
inbox_url: data["inbox"],
outbox_url: data["outbox"],
following_url: data["following"],
followers_url: data["followers"],
shared_inbox_url: data["endpoints"]["sharedInbox"],
domain: URI.parse(data["id"]).host,
manually_approves_followers: data["manuallyApprovesFollowers"],
type: data["type"]
}
{:ok, actor_data}
end
@doc """
@@ -57,18 +64,51 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Actor do
@impl Converter
@spec model_to_as(ActorModel.t()) :: map
def model_to_as(%ActorModel{} = actor) do
%{
"type" => Atom.to_string(actor.type),
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"preferred_username" => actor.preferred_username,
actor_data = %{
"id" => actor.url,
"type" => actor.type,
"preferredUsername" => actor.preferred_username,
"name" => actor.name,
"summary" => actor.summary,
"following" => ActorModel.build_url(actor.preferred_username, :following),
"followers" => ActorModel.build_url(actor.preferred_username, :followers),
"inbox" => ActorModel.build_url(actor.preferred_username, :inbox),
"outbox" => ActorModel.build_url(actor.preferred_username, :outbox),
"id" => ActorModel.build_url(actor.preferred_username, :page),
"url" => actor.url
"following" => actor.following_url,
"followers" => actor.followers_url,
"inbox" => actor.inbox_url,
"outbox" => actor.outbox_url,
"url" => actor.url,
"endpoints" => %{
"sharedInbox" => actor.shared_inbox_url
},
"manuallyApprovesFollowers" => actor.manually_approves_followers,
"publicKey" => %{
"id" => "#{actor.url}#main-key",
"owner" => actor.url,
"publicKeyPem" =>
if(is_nil(actor.domain) and not is_nil(actor.keys),
do: Utils.pem_to_public_key_pem(actor.keys),
else: actor.keys
)
}
}
actor_data =
if is_nil(actor.avatar) do
actor_data
else
Map.put(actor_data, "icon", %{
"type" => "Image",
"mediaType" => actor.avatar.content_type,
"url" => actor.avatar.url
})
end
if is_nil(actor.banner) do
actor_data
else
Map.put(actor_data, "image", %{
"type" => "Image",
"mediaType" => actor.banner.content_type,
"url" => actor.banner.url
})
end
end
end

View File

@@ -12,6 +12,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible}
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
alias Mobilizon.Tombstone, as: TombstoneModel
require Logger
@@ -32,9 +33,11 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
Logger.debug("We're converting raw ActivityStream data to a comment entity")
Logger.debug(inspect(object))
with {:ok, %Actor{id: actor_id}} <- ActivityPub.get_or_fetch_actor_by_url(object["actor"]),
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])},
{:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(object["tag"])} do
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
{:ok, %Actor{id: actor_id}} <- ActivityPub.get_or_fetch_actor_by_url(author_url),
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(Map.get(object, "tag", []))},
{:mentions, mentions} <-
{:mentions, ConverterUtils.fetch_mentions(Map.get(object, "tag", []))} do
Logger.debug("Inserting full comment")
Logger.debug(inspect(object))
@@ -70,6 +73,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
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} ->
@@ -106,6 +110,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
"to" => to,
"cc" => [],
"content" => comment.text,
"mediaType" => "text/html",
"actor" => comment.actor.url,
"attributedTo" => comment.actor.url,
"uuid" => comment.uuid,
@@ -114,23 +119,27 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
ConverterUtils.build_mentions(comment.mentions) ++ ConverterUtils.build_tags(comment.tags)
}
if comment.in_reply_to_comment do
object |> Map.put("inReplyTo", comment.in_reply_to_comment.url || comment.event.url)
else
object
cond do
comment.in_reply_to_comment ->
Map.put(object, "inReplyTo", comment.in_reply_to_comment.url)
comment.event ->
Map.put(object, "inReplyTo", comment.event.url)
true ->
object
end
end
@impl Converter
@spec model_to_as(CommentModel.t()) :: map
@doc """
A "soft-deleted" comment is a tombstone
"""
def model_to_as(%CommentModel{} = comment) do
%{
"type" => "Tombstone",
"uuid" => comment.uuid,
"id" => comment.url,
"published" => comment.inserted_at,
"updated" => comment.updated_at,
"deleted" => comment.deleted_at
}
Convertible.model_to_as(%TombstoneModel{
uri: comment.url,
inserted_at: comment.deleted_at
})
end
end

View File

@@ -6,14 +6,13 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
internal one, and back.
"""
alias Mobilizon.{Addresses, Media}
alias Mobilizon.Addresses
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Events.EventOptions
alias Mobilizon.Media.Picture
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils}
alias Mobilizon.Service.ActivityPub.{Converter, Convertible}
alias Mobilizon.Service.ActivityPub.Converter.Address, as: AddressConverter
alias Mobilizon.Service.ActivityPub.Converter.Picture, as: PictureConverter
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
@@ -37,26 +36,25 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
Logger.debug("event as_to_model_data")
Logger.debug(inspect(object))
with {:actor, {:ok, %Actor{id: actor_id}}} <-
{:actor, ActivityPub.get_or_fetch_actor_by_url(object["actor"])},
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
{:actor, {:ok, %Actor{id: actor_id, domain: actor_domain}}} <-
{:actor, ActivityPub.get_or_fetch_actor_by_url(author_url)},
{:address, address_id} <-
{:address, get_address(object["location"])},
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])},
{:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(object["tag"])},
{:visibility, visibility} <- {:visibility, get_visibility(object)},
{:options, options} <- {:options, get_options(object)} do
picture_id =
with true <- Map.has_key?(object, "attachment") && length(object["attachment"]) > 0,
%Picture{id: picture_id} <-
Media.get_picture_by_url(
object["attachment"]
|> hd
|> Map.get("url")
|> hd
|> Map.get("href")
) do
{:ok, %Picture{id: picture_id}} <-
object["attachment"]
|> hd
|> PictureConverter.find_or_create_picture(actor_id) do
picture_id
else
_ -> nil
_err ->
nil
end
entity = %{
@@ -68,16 +66,20 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
ends_on: object["endTime"],
category: object["category"],
visibility: visibility,
join_options: Map.get(object, "joinOptions", "free"),
join_options: Map.get(object, "joinMode", "free"),
local: is_nil(actor_domain),
options: options,
status: object["status"],
status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(),
online_address: object["onlineAddress"],
phone_address: object["phoneAddress"],
draft: object["draft"] || false,
draft: false,
url: object["id"],
uuid: object["uuid"],
tags: tags,
physical_address_id: address_id
mentions: mentions,
physical_address_id: address_id,
updated_at: object["updated"],
publish_at: object["published"]
}
{:ok, entity}
@@ -108,14 +110,17 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
"uuid" => event.uuid,
"category" => event.category,
"content" => event.description,
"publish_at" => (event.publish_at || event.inserted_at) |> date_to_string(),
"updated_at" => event.updated_at |> date_to_string(),
"published" => (event.publish_at || event.inserted_at) |> date_to_string(),
"updated" => event.updated_at |> date_to_string(),
"mediaType" => "text/html",
"startTime" => event.begins_on |> date_to_string(),
"joinOptions" => to_string(event.join_options),
"joinMode" => to_string(event.join_options),
"endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> ConverterUtils.build_tags(),
"draft" => event.draft,
"maximumAttendeeCapacity" => event.options.maximum_attendee_capacity,
"repliesModerationOption" => event.options.comment_moderation,
# "draft" => event.draft,
"ical:status" => event.status |> to_string |> String.upcase(),
"id" => event.url,
"url" => event.url
}
@@ -133,17 +138,10 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
# Get only elements that we have in EventOptions
@spec get_options(map) :: map
defp get_options(object) do
keys =
EventOptions
|> struct
|> Map.keys()
|> List.delete(:__struct__)
|> Enum.map(&Utils.camelize/1)
Enum.reduce(object, %{}, fn {key, value}, acc ->
(!is_nil(value) && key in keys && Map.put(acc, Utils.underscore(key), value)) ||
acc
end)
%{
maximum_attendee_capacity: object["maximumAttendeeCapacity"],
comment_moderation: object["repliesModerationOption"]
}
end
@spec get_address(map | binary | nil) :: integer | nil
@@ -186,13 +184,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
@ap_public "https://www.w3.org/ns/activitystreams#Public"
defp get_visibility(object) do
cond do
@ap_public in object["to"] -> :public
@ap_public in object["cc"] -> :unlisted
true -> :private
end
end
defp get_visibility(object), do: if(@ap_public in object["to"], do: :public, else: :unlisted)
@spec date_to_string(DateTime.t() | nil) :: String.t()
defp date_to_string(nil), do: nil

View File

@@ -15,6 +15,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Flag do
alias Mobilizon.Reports.Report
alias Mobilizon.Service.ActivityPub.Converter
alias Mobilizon.Service.ActivityPub.Convertible
alias Mobilizon.Service.ActivityPub.Relay
@behaviour Converter
@@ -42,8 +43,6 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Flag do
end
end
@audience %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []}
@doc """
Convert an event struct to an ActivityStream representation
"""
@@ -54,17 +53,13 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Flag do
object = if report.event, do: object ++ [report.event.url], else: object
audience =
if report.local, do: @audience, else: Map.put(@audience, "cc", [report.reported.url])
%{
"type" => "Flag",
"actor" => report.reporter.url,
"actor" => Relay.get_actor().url,
"id" => report.url,
"content" => report.content,
"object" => object
}
|> Map.merge(audience)
end
@spec as_to_model(map) :: map
@@ -91,7 +86,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Flag do
end
end),
# Remove the reported user from the object list.
# Remove the reported actor and the event from the object list.
comments <-
Enum.filter(objects, fn url ->
!(url == reported.url || (!is_nil(event) && event.url == url))

View File

@@ -15,14 +15,48 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Picture do
def model_to_as(%PictureModel{file: file}) do
%{
"type" => "Document",
"url" => [
%{
"type" => "Link",
"mediaType" => file.content_type,
"href" => file.url
}
],
"mediaType" => file.content_type,
"url" => file.url,
"name" => file.name
}
end
@doc """
Save picture data from raw data and return AS Link data.
"""
def find_or_create_picture(%{"type" => "Link", "href" => url}, actor_id),
do: find_or_create_picture(url, actor_id)
def find_or_create_picture(
%{"type" => "Document", "url" => picture_url, "name" => name},
actor_id
)
when is_bitstring(picture_url) do
with {:ok, %HTTPoison.Response{body: body}} <- HTTPoison.get(picture_url),
{:ok,
%{
name: name,
url: url,
content_type: content_type,
size: size
}} <-
MobilizonWeb.Upload.store(%{body: body, name: name}),
{:picture_exists, nil} <- {:picture_exists, Mobilizon.Media.get_picture_by_url(url)} do
Mobilizon.Media.create_picture(%{
"file" => %{
"url" => url,
"name" => name,
"content_type" => content_type,
"size" => size
},
"actor_id" => actor_id
})
else
{:picture_exists, %PictureModel{file: _file} = picture} ->
{:ok, picture}
err ->
err
end
end
end

View File

@@ -0,0 +1,40 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Tombstone do
@moduledoc """
Comment converter.
This module allows to convert Tombstone models to ActivityStreams data
"""
alias Mobilizon.Tombstone, as: TombstoneModel
alias Mobilizon.Service.ActivityPub.{Converter, Convertible}
require Logger
@behaviour Converter
defimpl Convertible, for: TombstoneModel do
alias Mobilizon.Service.ActivityPub.Converter.Tombstone, as: TombstoneConverter
defdelegate model_to_as(comment), to: TombstoneConverter
end
@doc """
Make an AS tombstone object from an existing `Tombstone` structure.
"""
@impl Converter
@spec model_to_as(TombstoneModel.t()) :: map
def model_to_as(%TombstoneModel{} = tombstone) do
%{
"type" => "Tombstone",
"id" => tombstone.uri,
"deleted" => tombstone.inserted_at
}
end
@doc """
Converting an Tombstone to an object makes no sense, nevertheless…
"""
@impl Converter
@spec as_to_model_data(map) :: map
def as_to_model_data(object), do: object
end

View File

@@ -14,6 +14,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Utils do
@spec fetch_tags([String.t()]) :: [Tag.t()]
def fetch_tags(tags) when is_list(tags) do
Logger.debug("fetching tags")
Logger.debug(inspect(tags))
tags |> Enum.flat_map(&fetch_tag/1) |> Enum.uniq() |> Enum.map(&existing_tag_or_data/1)
end
@@ -64,6 +65,8 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Utils do
}
end
defp fetch_tag(%{title: title}), do: [title]
defp fetch_tag(tag) when is_map(tag) do
case tag["type"] do
"Hashtag" ->

View File

@@ -9,27 +9,37 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Activity, Transmogrifier}
alias Mobilizon.Service.WebFinger
alias MobilizonWeb.API.Follows
require Logger
def init() do
# Wait for everything to settle.
Process.sleep(1000 * 5)
get_actor()
end
@spec get_actor() :: Actor.t() | {:error, Ecto.Changeset.t()}
def get_actor do
with {:ok, %Actor{} = actor} <-
Actors.get_or_create_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
Actors.get_or_create_instance_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
actor
end
end
def follow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
@spec follow(String.t()) :: {:ok, Activity.t(), Follower.t()}
def follow(address) do
with {:ok, target_instance} <- fetch_actor(address),
%Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance),
{:ok, activity} <- Follows.follow(local_actor, target_actor) do
{:ok, activity, follow} <- Follows.follow(local_actor, target_actor) do
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
{:ok, activity, follow}
else
e ->
Logger.warn("Error while following remote instance: #{inspect(e)}")
@@ -37,12 +47,14 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
end
end
def unfollow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
@spec unfollow(String.t()) :: {:ok, Activity.t(), Follower.t()}
def unfollow(address) do
with {:ok, target_instance} <- fetch_actor(address),
%Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance),
{:ok, activity} <- Follows.unfollow(local_actor, target_actor) do
{:ok, activity, follow} <- Follows.unfollow(local_actor, target_actor) do
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
{:ok, activity, follow}
else
e ->
Logger.warn("Error while unfollowing remote instance: #{inspect(e)}")
@@ -50,30 +62,38 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
end
end
def accept(target_instance) do
with %Actor{} = local_actor <- get_actor(),
@spec accept(String.t()) :: {:ok, Activity.t(), Follower.t()}
def accept(address) do
Logger.debug("We're trying to accept a relay subscription")
with {:ok, target_instance} <- fetch_actor(address),
%Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance),
{:ok, activity} <- Follows.accept(target_actor, local_actor) do
{:ok, activity}
{:ok, activity, follow} <- Follows.accept(target_actor, local_actor) do
{:ok, activity, follow}
end
end
# def reject(target_instance) do
# with %Actor{} = local_actor <- get_actor(),
# {:ok, %Actor{} = target_actor} <- Activity.get_or_fetch_actor_by_url(target_instance),
# {:ok, activity} <- Follows.reject(target_actor, local_actor) do
# {:ok, activity}
# end
# end
def reject(address) do
Logger.debug("We're trying to reject a relay subscription")
with {:ok, target_instance} <- fetch_actor(address),
%Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance),
{:ok, activity, follow} <- Follows.reject(target_actor, local_actor) do
{:ok, activity, follow}
end
end
@doc """
Publish an activity to all relays following this instance
"""
def publish(%Activity{data: %{"object" => object}} = _activity) do
with %Actor{id: actor_id} = actor <- get_actor(),
{:ok, object} <-
Transmogrifier.fetch_obj_helper_as_activity_streams(object) do
ActivityPub.announce(actor, object, "#{object["id"]}/announces/#{actor_id}", true, false)
{object, object_id} <- fetch_object(object),
id <- "#{object_id}/announces/#{actor_id}" do
Logger.info("Publishing activity #{id} to all relays")
ActivityPub.announce(actor, object, id, true, false)
else
e ->
Logger.error("Error while getting local instance actor: #{inspect(e)}")
@@ -85,4 +105,51 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
Logger.debug(inspect(err))
nil
end
defp fetch_object(object) when is_map(object) do
with {:ok, object} <- Transmogrifier.fetch_obj_helper_as_activity_streams(object) do
{object, object["id"]}
end
end
defp fetch_object(object) when is_bitstring(object), do: {object, object}
@spec fetch_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()}
# Dirty hack
defp fetch_actor("https://" <> address), do: fetch_actor(address)
defp fetch_actor("http://" <> address), do: fetch_actor(address)
defp fetch_actor(address) do
%URI{host: host} = URI.parse("http://" <> address)
cond do
String.contains?(address, "@") ->
check_actor(address)
!is_nil(host) ->
check_actor("relay@#{host}")
true ->
{:error, "Bad URL"}
end
end
@spec check_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()}
defp check_actor(username_and_domain) do
case Actors.get_actor_by_name(username_and_domain) do
%Actor{url: url} -> {:ok, url}
nil -> finger_actor(username_and_domain)
end
end
@spec finger_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()}
defp finger_actor(nickname) do
case WebFinger.finger(nickname) do
{:ok, %{"url" => url}} when not is_nil(url) ->
{:ok, url}
_e ->
{:error, "No ActivityPub URL found in WebFinger"}
end
end
end

View File

@@ -20,108 +20,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
require Logger
def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor
end
def get_actor(%{"actor" => actor}) when is_list(actor) do
if is_binary(Enum.at(actor, 0)) do
Enum.at(actor, 0)
else
actor
|> Enum.find(fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|> Map.get("id")
end
end
def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
id
end
def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
get_actor(%{"actor" => actor})
end
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object) do
object
|> Map.put("actor", object["attributedTo"])
|> fix_attachments
# |> fix_in_reply_to
# |> fix_tag
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
when not is_nil(in_reply_to) and is_bitstring(in_reply_to) do
in_reply_to |> do_fix_in_reply_to(object)
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
when not is_nil(in_reply_to) and is_map(in_reply_to) do
if is_bitstring(in_reply_to["id"]) do
in_reply_to["id"] |> do_fix_in_reply_to(object)
end
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
when not is_nil(in_reply_to) and is_list(in_reply_to) do
if is_bitstring(Enum.at(in_reply_to, 0)) do
in_reply_to |> Enum.at(0) |> do_fix_in_reply_to(object)
end
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
when not is_nil(in_reply_to) do
Logger.warn("inReplyTo ID seem incorrect: #{inspect(in_reply_to)}")
do_fix_in_reply_to("", object)
end
def fix_in_reply_to(object), do: object
def do_fix_in_reply_to(in_reply_to_id, object) do
case fetch_obj_helper(in_reply_to_id) do
{:ok, replied_object} ->
object
|> Map.put("inReplyTo", replied_object.url)
{:error, {:error, :not_supported}} ->
Logger.info("Object reply origin has not a supported type")
object
e ->
Logger.warn("Couldn't fetch #{in_reply_to_id} #{inspect(e)}")
object
end
end
def fix_attachments(object) do
attachments =
(object["attachment"] || [])
|> Enum.map(fn data ->
url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
Map.put(data, "url", url)
end)
object
|> Map.put("attachment", attachments)
end
def fix_tag(object) do
tags =
(object["tag"] || [])
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
combined = (object["tag"] || []) ++ tags
object
|> Map.put("tag", combined)
end
def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error
@@ -135,6 +33,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
additional: %{
"cc" => [params["reported"].url]
},
event_id: if(is_nil(params["event"]), do: nil, else: params["event"].id || nil),
local: false
}
@@ -158,7 +57,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
Logger.info("Handle incoming to create notes")
with {:ok, object_data} <-
object |> fix_object() |> Converter.Comment.as_to_model_data(),
object |> Converter.Comment.as_to_model_data(),
{:existing_comment, {:error, :comment_not_found}} <-
{:existing_comment, Events.get_comment_from_url_with_preload(object_data.url)},
{:ok, %Activity{} = activity, %Comment{} = comment} <-
@@ -186,7 +85,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
Logger.info("Handle incoming to create event")
with {:ok, object_data} <-
object |> fix_object() |> Converter.Event.as_to_model_data(),
object |> Converter.Event.as_to_model_data(),
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Event{} = event} <-
ActivityPub.create(:event, object_data, false) do
@@ -273,36 +172,25 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
#
# def handle_incoming(
# %{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data
# ) do
# with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
# {:ok, object} <-
# fetch_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
# {:ok, activity, object} <- ActivityPub.like(actor, object, id, false) do
# {:ok, activity}
# else
# _e -> :error
# end
# end
# #
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => _id} = data
%{"type" => "Announce", "object" => object, "actor" => _actor, "id" => _id} = data
) do
with actor <- get_actor(data),
# TODO: Is the following line useful?
{:ok, %Actor{} = _actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
{:ok, %Actor{id: actor_id} = _actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
:ok <- Logger.debug("Fetching contained object"),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object),
:ok <- Logger.debug("Handling contained object"),
create_data <-
make_create_data(object),
:ok <- Logger.debug(inspect(object)),
{:ok, _activity, object} <- handle_incoming(create_data),
{:ok, _activity, entity} <- handle_incoming(create_data),
:ok <- Logger.debug("Finished processing contained object"),
{:ok, activity} <- ActivityPub.create_activity(data, false) do
{:ok, activity, object}
{:ok, activity} <- ActivityPub.create_activity(data, false),
{:ok, %Actor{id: object_owner_actor_id}} <- Actors.get_actor_by_url(object["actor"]),
{:ok, %Mobilizon.Share{} = _share} <-
Mobilizon.Share.create(object["id"], actor_id, object_owner_actor_id) do
{:ok, activity, entity}
else
e ->
Logger.debug(inspect(e))
@@ -318,7 +206,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
when object_type in ["Person", "Group", "Application", "Service", "Organization"] do
with {:ok, %Actor{} = old_actor} <- Actors.get_actor_by_url(object["id"]),
{:ok, object_data} <-
object |> fix_object() |> Converter.Actor.as_to_model_data(),
object |> Converter.Actor.as_to_model_data(),
{:ok, %Activity{} = activity, %Actor{} = new_actor} <-
ActivityPub.update(:actor, old_actor, object_data, false) do
{:ok, activity, new_actor}
@@ -331,12 +219,15 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => _actor} =
_update
update_data
) do
with %Event{} = old_event <-
Events.get_event_by_url(object["id"]),
with actor <- get_actor(update_data),
{:ok, %Actor{url: actor_url}} <- Actors.get_actor_by_url(actor),
{:ok, %Event{} = old_event} <-
object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
{:ok, object_data} <-
object |> fix_object() |> Converter.Event.as_to_model_data(),
object |> Converter.Event.as_to_model_data(),
{:origin_check, true} <- {:origin_check, origin_check?(actor_url, update_data)},
{:ok, %Activity{} = activity, %Event{} = new_event} <-
ActivityPub.update(:event, old_event, object_data, false) do
{:ok, activity, new_event}
@@ -396,16 +287,18 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data
) do
object_id = Utils.get_url(object)
with actor <- get_actor(data),
{:ok, %Actor{url: _actor_url}} <- Actors.get_actor_by_url(actor),
{:ok, object} <- fetch_obj_helper(object_id),
# TODO : Validate that DELETE comes indeed form right domain (see above)
# :ok <- contain_origin(actor_url, object.data),
{:ok, %Actor{url: actor_url}} <- Actors.get_actor_by_url(actor),
object_id <- Utils.get_url(object),
{:origin_check, true} <- {:origin_check, origin_check_from_id?(actor_url, object_id)},
{:ok, object} <- ActivityPub.fetch_object_from_url(object_id),
{:ok, activity, object} <- ActivityPub.delete(object, false) do
{:ok, activity, object}
else
{:origin_check, false} ->
Logger.warn("Object origin check failed")
:error
e ->
Logger.debug(inspect(e))
:error
@@ -413,12 +306,13 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
def handle_incoming(
%{"type" => "Join", "object" => object, "actor" => _actor, "id" => _id} = data
%{"type" => "Join", "object" => object, "actor" => _actor, "id" => id} = data
) do
with actor <- get_actor(data),
{:ok, %Actor{url: _actor_url} = actor} <- Actors.get_actor_by_url(actor),
{:ok, object} <- fetch_obj_helper(object),
{:ok, activity, object} <- ActivityPub.join(object, actor, false) do
object <- Utils.get_url(object),
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
{:ok, activity, object} <- ActivityPub.join(object, actor, false, %{url: id}) do
{:ok, activity, object}
else
e ->
@@ -432,7 +326,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
) do
with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_actor_by_url(actor),
{:ok, object} <- fetch_obj_helper(object),
object <- Utils.get_url(object),
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
{:ok, activity, object} <- ActivityPub.leave(object, actor, false) do
{:ok, activity, object}
else
@@ -487,7 +382,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
ActivityPub.accept(
:follow,
follow,
%{approved: true},
false
) do
{:ok, activity, follow}
@@ -511,23 +405,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
Handle incoming `Reject` activities wrapping a `Follow` activity
"""
def do_handle_incoming_reject_following(follow_object, %Actor{} = actor) do
with {:follow,
{:ok,
%Follower{approved: false, actor: follower, id: follow_id, target_actor: followed} =
follow}} <-
with {:follow, {:ok, %Follower{approved: false, target_actor: followed} = follow}} <-
{:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, activity, _} <-
ActivityPub.reject(
%{
to: [follower.url],
actor: actor.url,
object: follow_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/reject/follow/#{follow_id}"
),
{:ok, %Follower{}} <- Actors.delete_follower(follow) do
ActivityPub.reject(:follow, follow) do
{:ok, activity, follow}
else
{:follow, _} ->
@@ -547,7 +429,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# Handle incoming `Accept` activities wrapping a `Join` activity on an event
defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
with {:join_event, {:ok, %Participant{role: :not_approved, event: event} = participant}} <-
with {:join_event, {:ok, %Participant{role: role, event: event} = participant}}
when role in [:not_approved, :rejected] <-
{:join_event, get_participant(join_object)},
# TODO: The actor that accepts the Join activity may another one that the event organizer ?
# Or maybe for groups it's the group that sends the Accept activity
@@ -556,7 +439,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
ActivityPub.accept(
:join,
participant,
%{role: :participant},
false
),
:ok <-
@@ -587,32 +469,20 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# Handle incoming `Reject` activities wrapping a `Join` activity on an event
defp do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do
with {:join_event,
{:ok,
%Participant{role: :not_approved, actor: actor, id: join_id, event: event} =
participant}} <-
with {:join_event, {:ok, %Participant{event: event, role: role} = participant}}
when role != :rejected <-
{:join_event, get_participant(join_object)},
# TODO: The actor that accepts the Join activity may another one that the event organizer ?
# Or maybe for groups it's the group that sends the Accept activity
{:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
{:ok, activity, _} <-
ActivityPub.reject(
%{
to: [actor.url],
actor: actor_accepting.url,
object: join_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/reject/join/#{join_id}"
),
{:ok, %Participant{role: :rejected} = participant} <-
Events.update_participant(participant, %{"role" => :rejected}),
{:ok, activity, participant} <-
ActivityPub.reject(:join, participant, false),
:ok <- Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant}
else
{:join_event, {:ok, %Participant{role: :participant}}} ->
Logger.debug(
"Tried to handle an Reject activity on a Join activity with a event object but the participant is already validated"
{:join_event, {:ok, %Participant{role: :rejected}}} ->
Logger.warn(
"Tried to handle an Reject activity on a Join activity with a event object but the participant is already rejected"
)
nil
@@ -662,49 +532,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) do
with false <- String.starts_with?(in_reply_to, "http"),
{:ok, replied_to_object} <- fetch_obj_helper(in_reply_to) do
Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
else
_e -> object
end
end
def set_reply_to_uri(obj), do: obj
#
# # Prepares the object of an outgoing create activity.
def prepare_object(object) do
object
# |> set_sensitive
# |> add_hashtags
|> add_mention_tags
# |> add_emoji_tags
|> add_attributed_to
# |> prepare_attachments
|> set_reply_to_uri
end
@doc """
internal -> Mastodon
"""
def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
Logger.debug("Prepare outgoing for a note creation")
object =
object
|> prepare_object
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
Logger.debug("Finished prepare outgoing for a note creation")
{:ok, data}
end
def prepare_outgoing(%{"type" => _type} = data) do
data =
data
@@ -713,145 +540,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
{:ok, data}
end
# def prepare_outgoing(%Event{} = event) do
# event =
# event
# |> Map.from_struct()
# |> Map.drop([:__meta__])
# |> Map.put(:"@context", "https://www.w3.org/ns/activitystreams")
# |> prepare_object
# {:ok, event}
# end
# def prepare_outgoing(%Comment{} = comment) do
# comment =
# comment
# |> Map.from_struct()
# |> Map.drop([:__meta__])
# |> Map.put(:"@context", "https://www.w3.org/ns/activitystreams")
# |> prepare_object
# {:ok, comment}
# end
#
# def maybe_fix_object_url(data) do
# if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
# case ActivityPub.fetch_object_from_id(data["object"]) do
# {:ok, relative_object} ->
# if relative_object.data["external_url"] do
# data =
# data
# |> Map.put("object", relative_object.data["external_url"])
# else
# data
# end
#
# e ->
# Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
# data
# end
# else
# data
# end
# end
#
def add_hashtags(object) do
tags =
(object["tag"] || [])
|> Enum.map(fn tag ->
%{
"href" => MobilizonWeb.Endpoint.url() <> "/tags/#{tag}",
"name" => "##{tag}",
"type" => "Hashtag"
}
end)
object
|> Map.put("tag", tags)
end
def add_mention_tags(object) do
Logger.debug("add mention tags")
Logger.debug(inspect(object))
recipients =
(object["to"] ++ (object["cc"] || [])) -- ["https://www.w3.org/ns/activitystreams#Public"]
mentions =
recipients
|> Enum.filter(& &1)
|> Enum.map(fn url ->
case Actors.get_actor_by_url(url) do
{:ok, actor} -> actor
_ -> nil
end
end)
|> Enum.filter(& &1)
|> Enum.map(fn actor ->
%{
"type" => "Mention",
"href" => actor.url,
"name" => "@#{Actor.preferred_username_and_domain(actor)}"
}
end)
tags = object["tag"] || []
object
|> Map.put("tag", tags ++ mentions)
end
#
# # TODO: we should probably send mtime instead of unix epoch time for updated
# def add_emoji_tags(object) do
# tags = object["tag"] || []
# emoji = object["emoji"] || []
#
# out =
# emoji
# |> Enum.map(fn {name, url} ->
# %{
# "icon" => %{"url" => url, "type" => "Image"},
# "name" => ":" <> name <> ":",
# "type" => "Emoji",
# "updated" => "1970-01-01T00:00:00Z",
# "id" => url
# }
# end)
#
# object
# |> Map.put("tag", tags ++ out)
# end
#
#
# def set_sensitive(object) do
# tags = object["tag"] || []
# Map.put(object, "sensitive", "nsfw" in tags)
# end
#
def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"]
object |> Map.put("attributedTo", attributed_to)
end
#
# def prepare_attachments(object) do
# attachments =
# (object["attachment"] || [])
# |> Enum.map(fn data ->
# [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
# %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
# end)
#
# object
# |> Map.put("attachment", attachments)
# end
@spec fetch_obj_helper(map() | String.t()) :: Event.t() | Comment.t() | Actor.t() | any()
def fetch_obj_helper(object) do
Logger.debug("fetch_obj_helper")
@@ -862,7 +550,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
{:ok, object}
err ->
Logger.info("Error while fetching #{inspect(object)}")
Logger.warn("Error while fetching #{inspect(object)}")
{:error, err}
end
end

View File

@@ -8,20 +8,11 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
# Various ActivityPub related utils.
"""
alias Ecto.Changeset
alias Mobilizon.{Actors, Addresses, Events, Reports, Users}
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
alias Mobilizon.Events.{Comment, Event}
alias Mobilizon.Media.Picture
alias Mobilizon.Reports.Report
alias Mobilizon.Service.ActivityPub.{Activity, Converter}
alias Mobilizon.Service.Federator
alias Mobilizon.Storage.Repo
alias MobilizonWeb.{Email, Endpoint}
alias MobilizonWeb.Router.Helpers, as: Routes
require Logger
@@ -37,12 +28,31 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://litepub.github.io/litepub/context.jsonld",
"https://litepub.social/litepub/context.jsonld",
%{
"sc" => "http://schema.org#",
"ical" => "http://www.w3.org/2002/12/cal/ical#",
"Hashtag" => "as:Hashtag",
"category" => "sc:category",
"uuid" => "sc:identifier"
"uuid" => "sc:identifier",
"maximumAttendeeCapacity" => "sc:maximumAttendeeCapacity",
"mz" => "https://joinmobilizon.org/ns#",
"repliesModerationOptionType" => %{
"@id" => "mz:repliesModerationOptionType",
"@type" => "rdfs:Class"
},
"repliesModerationOption" => %{
"@id" => "mz:repliesModerationOption",
"@type" => "mz:repliesModerationOptionType"
},
"joinModeType" => %{
"@id" => "mz:joinModeType",
"@type" => "rdfs:Class"
},
"joinMode" => %{
"@id" => "mz:joinMode",
"@type" => "mz:joinModeType"
}
}
]
}
@@ -112,128 +122,56 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
Map.put_new_lazy(map, "published", &make_date/0)
end
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(object_data)
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => "Event"} = object_data, "type" => "Create"})
when is_map(object_data) do
with {:ok, object_data} <-
Converter.Event.as_to_model_data(object_data),
{:ok, %Event{} = event} <- Events.create_event(object_data) do
{:ok, event}
end
def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor
end
def insert_full_object(%{"object" => %{"type" => "Group"} = object_data, "type" => "Create"})
when is_map(object_data) do
with object_data <-
Map.put(object_data, "preferred_username", object_data["preferredUsername"]),
{:ok, %Actor{} = group} <- Actors.create_group(object_data) do
{:ok, group}
end
end
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => "Note"} = object_data, "type" => "Create"})
when is_map(object_data) do
with data <- Converter.Comment.as_to_model_data(object_data),
{:ok, %Comment{} = comment} <- Events.create_comment(data) do
{:ok, comment}
def get_actor(%{"actor" => actor}) when is_list(actor) do
if is_binary(Enum.at(actor, 0)) do
Enum.at(actor, 0)
else
err ->
Logger.error("Error while inserting a remote comment inside database")
Logger.debug(inspect(err))
{:error, err}
actor
|> Enum.find(fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|> Map.get("id")
end
end
def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
id
end
def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
get_actor(%{"actor" => actor})
end
@doc """
Inserts a full object if it is contained in an activity.
Checks that an incoming AP object's actor matches the domain it came from.
"""
def insert_full_object(%{"type" => "Flag"} = object_data)
when is_map(object_data) do
with data <- Converter.Flag.as_to_model_data(object_data),
{:ok, %Report{} = report} <- Reports.create_report(data) do
Enum.each(Users.list_moderators(), fn moderator ->
moderator
|> Email.Admin.report(report)
|> Email.Mailer.deliver_later()
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))
{:ok, report}
else
err ->
Logger.error("Error while inserting report inside database")
Logger.debug(inspect(err))
{:error, err}
end
compare_uris?(actor_uri, id_uri)
end
def insert_full_object(_), do: {:ok, nil}
def origin_check?(_id, %{"actor" => nil}), do: false
@doc """
Update an object
"""
@spec update_object(struct(), map()) :: {:ok, struct()} | any()
def update_object(object, object_data)
def origin_check?(id, %{"attributedTo" => actor} = params),
do: origin_check?(id, Map.put(params, "actor", actor))
def update_object(event_url, %{
"object" => %{"type" => "Event"} = object_data,
"type" => "Update"
})
when is_map(object_data) do
with {:event_not_found, %Event{} = event} <-
{:event_not_found, Events.get_event_by_url(event_url)},
{:ok, object_data} <- Converter.Event.as_to_model_data(object_data),
{:ok, %Event{} = event} <- Events.update_event(event, object_data) do
{:ok, event}
end
def origin_check?(_id, _data), do: false
defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri), do: id_uri.host == other_uri.host
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)
compare_uris?(id_uri, other_uri)
end
def update_object(actor_url, %{
"object" => %{"type" => type_actor} = object_data,
"type" => "Update"
})
when is_map(object_data) and type_actor in @actor_types do
with {:ok, %Actor{} = actor} <- Actors.get_actor_by_url(actor_url),
object_data <- Converter.Actor.as_to_model_data(object_data),
{:ok, %Actor{} = actor} <- Actors.update_actor(actor, object_data) do
{:ok, actor}
end
end
def update_object(_, _), do: {:ok, nil}
#### Like-related helpers
# @doc """
# Returns an existing like if a user already liked an object
# """
# def get_existing_like(actor, %{data: %{"id" => id}}) do
# query =
# from(
# activity in Activity,
# where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
# # this is to use the index
# where:
# fragment(
# "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
# activity.data,
# activity.data,
# ^id
# ),
# where: fragment("(?)->>'type' = 'Like'", activity.data)
# )
#
# Repo.one(query)
# end
def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id),
do: origin_check_from_id?(id, other_id)
@doc """
Save picture data from %Plug.Upload{} and return AS Link data.
@@ -284,255 +222,6 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
def make_picture_data(nil), do: nil
@doc """
Make an AP event object from an set of values
"""
@spec make_event_data(
String.t(),
map(),
String.t(),
String.t(),
map(),
list(),
map(),
String.t()
) :: map()
def make_event_data(
actor,
%{to: to, cc: cc} = _audience,
title,
content_html,
picture \\ nil,
tags \\ [],
metadata \\ %{},
uuid \\ nil,
url \\ nil
) do
Logger.debug("Making event data")
uuid = uuid || Ecto.UUID.generate()
res = %{
"type" => "Event",
"to" => to,
"cc" => cc || [],
"content" => content_html,
"name" => title,
"startTime" => metadata.begins_on,
"endTime" => metadata.ends_on,
"category" => metadata.category,
"actor" => actor,
"id" => url || Routes.page_url(Endpoint, :event, uuid),
"joinOptions" => metadata.join_options,
"status" => metadata.status,
"onlineAddress" => metadata.online_address,
"phoneAddress" => metadata.phone_address,
"draft" => metadata.draft,
"uuid" => uuid,
"tag" =>
tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end)
}
res =
if is_nil(metadata.physical_address),
do: res,
else: Map.put(res, "location", make_address_data(metadata.physical_address))
res =
if is_nil(picture), do: res, else: Map.put(res, "attachment", [make_picture_data(picture)])
if is_nil(metadata.options) do
res
else
options = Events.EventOptions |> struct(metadata.options) |> Map.from_struct()
Enum.reduce(options, res, fn {key, value}, acc ->
(!is_nil(value) && Map.put(acc, camelize(key), value)) ||
acc
end)
end
end
def make_address_data(%Address{} = address) do
# res = %{
# "type" => "Place",
# "name" => address.description,
# "id" => address.url,
# "address" => %{
# "type" => "PostalAddress",
# "streetAddress" => address.street,
# "postalCode" => address.postal_code,
# "addressLocality" => address.locality,
# "addressRegion" => address.region,
# "addressCountry" => address.country
# }
# }
#
# if is_nil(address.geom) do
# res
# else
# Map.put(res, "geo", %{
# "type" => "GeoCoordinates",
# "latitude" => address.geom.coordinates |> elem(0),
# "longitude" => address.geom.coordinates |> elem(1)
# })
# end
address.url
end
def make_address_data(address) when is_map(address) do
Address
|> struct(address)
|> make_address_data()
end
def make_address_data(address_url) when is_bitstring(address_url) do
with %Address{} = address <- Addresses.get_address_by_url(address_url) do
address.url
end
end
@doc """
Make an AP comment object from an set of values
"""
def make_comment_data(
actor,
to,
content_html,
# attachments,
inReplyTo \\ nil,
tags \\ [],
# _cw \\ nil,
cc \\ []
) do
Logger.debug("Making comment data")
uuid = Ecto.UUID.generate()
object = %{
"type" => "Note",
"to" => to,
"cc" => cc,
"content" => content_html,
# "summary" => cw,
# "attachment" => attachments,
"actor" => actor,
"id" => Routes.page_url(Endpoint, :comment, uuid),
"uuid" => uuid,
"tag" => tags |> Enum.uniq()
}
if inReplyTo do
object
|> Map.put("inReplyTo", inReplyTo)
else
object
end
end
def make_group_data(
actor,
to,
preferred_username,
content_html,
# attachments,
tags \\ [],
# _cw \\ nil,
cc \\ []
) do
uuid = Ecto.UUID.generate()
%{
"type" => "Group",
"to" => to,
"cc" => cc,
"summary" => content_html,
"attributedTo" => actor,
"preferredUsername" => preferred_username,
"id" => Actor.build_url(preferred_username, :page),
"uuid" => uuid,
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
}
end
#### Like-related helpers
@doc """
Returns an existing like if a user already liked an object
"""
# @spec get_existing_like(Actor.t, map()) :: nil
# def get_existing_like(%Actor{url: url} = actor, %{data: %{"id" => id}}) do
# nil
# end
# def make_like_data(%Actor{url: url} = actor, %{data: %{"id" => id}} = object, activity_id) do
# data = %{
# "type" => "Like",
# "actor" => url,
# "object" => id,
# "to" => [actor.followers_url, object.data["actor"]],
# "cc" => ["https://www.w3.org/ns/activitystreams#Public"],
# "context" => object.data["context"]
# }
# if activity_id, do: Map.put(data, "id", activity_id), else: data
# end
def update_element_in_object(property, element, object) do
with new_data <-
object.data
|> Map.put("#{property}_count", length(element))
|> Map.put("#{property}s", element),
changeset <- Changeset.change(object, data: new_data),
{:ok, object} <- Repo.update(changeset) do
{:ok, object}
end
end
# def update_likes_in_object(likes, object) do
# update_element_in_object("like", likes, object)
# end
#
# def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
# with likes <- [actor | object.data["likes"] || []] |> Enum.uniq() do
# update_likes_in_object(likes, object)
# end
# end
#
# def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
# with likes <- (object.data["likes"] || []) |> List.delete(actor) do
# update_likes_in_object(likes, object)
# end
# end
#### Follow-related helpers
@doc """
Makes a follow activity data for the given followed and follower
"""
def make_follow_data(%Actor{url: followed_id}, %Actor{url: follower_id}, activity_id) do
Logger.debug("Make follow data")
data = %{
"type" => "Follow",
"actor" => follower_id,
"to" => [followed_id],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => followed_id
}
data =
if activity_id,
do: Map.put(data, "id", activity_id),
else: data
Logger.debug(inspect(data))
data
end
#### Announce-related helpers
require Logger
@doc """
Make announce activity data for the given actor and object
"""
@@ -673,42 +362,6 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|> Map.merge(additional)
end
#### Flag-related helpers
@spec make_flag_data(map(), map()) :: map()
def make_flag_data(params, additional) do
object = [params.reported_actor_url] ++ params.comments_url
object = if params[:event_url], do: object ++ [params.event_url], else: object
%{
"type" => "Flag",
"id" => "#{MobilizonWeb.Endpoint.url()}/report/#{Ecto.UUID.generate()}",
"actor" => params.reporter_url,
"content" => params.content,
"object" => object,
"state" => "open"
}
|> Map.merge(additional)
end
def make_join_data(%Event{} = event, %Actor{} = actor) do
%{
"type" => "Join",
"id" => "#{actor.url}/join/event/id",
"actor" => actor.url,
"object" => event.url
}
end
def make_join_data(%Actor{type: :Group} = event, %Actor{} = actor) do
%{
"type" => "Join",
"id" => "#{actor.url}/join/group/id",
"actor" => actor.url,
"object" => event.url
}
end
@doc """
Make accept join activity data
"""
@@ -718,7 +371,6 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"type" => "Accept",
"to" => object["to"],
"cc" => object["cc"],
"actor" => object["actor"],
"object" => object,
"id" => object["id"] <> "/activity"
}
@@ -741,37 +393,39 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
end
end
@doc """
Converts PEM encoded keys to a private key representation
"""
def pem_to_private_key(pem) do
[private_key_code] = :public_key.pem_decode(pem)
:public_key.pem_entry_decode(private_key_code)
end
@doc """
Converts PEM encoded keys to a PEM public key representation
"""
def pem_to_public_key_pem(pem) do
public_key = pem_to_public_key(pem)
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
:public_key.pem_encode([public_key])
end
def camelize(word) when is_atom(word) do
camelize(to_string(word))
defp make_signature(id, date) do
uri = URI.parse(id)
signature =
Mobilizon.Service.ActivityPub.Relay.get_actor()
|> Mobilizon.Service.HTTPSignatures.Signature.sign(%{
"(request-target)": "get #{uri.path}",
host: uri.host,
date: date
})
[{:Signature, signature}]
end
def camelize(word) when is_bitstring(word) do
{first, rest} = String.split_at(Macro.camelize(word), 1)
String.downcase(first) <> rest
def sign_fetch(headers, id, date) do
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do
headers ++ make_signature(id, date)
else
headers
end
end
def underscore(word) when is_atom(word) do
underscore(to_string(word))
end
def underscore(word) when is_bitstring(word) do
Macro.underscore(word)
def maybe_date_fetch(headers, date) do
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do
headers ++ [{:Date, date}]
else
headers
end
end
end

View File

@@ -17,7 +17,10 @@ defmodule Mobilizon.Service.ActivityPub.Visibility do
def is_public?(%{data: %{"type" => "Tombstone"}}), do: false
def is_public?(%{data: data}), do: is_public?(data)
def is_public?(%Activity{data: data}), do: is_public?(data)
def is_public?(data) when is_map(data), do: @public in (data["to"] ++ (data["cc"] || []))
def is_public?(data) when is_map(data),
do: @public in (Map.get(data, "to", []) ++ Map.get(data, "cc", []))
def is_public?(%Comment{deleted_at: deleted_at}), do: !is_nil(deleted_at)
def is_public?(err), do: raise(ArgumentError, message: "Invalid argument #{inspect(err)}")
end

View File

@@ -34,16 +34,14 @@ defmodule Mobilizon.Service.Formatter do
def mention_handler("@" <> nickname, buffer, _opts, acc) do
case Actors.get_actor_by_name(nickname) do
%Actor{preferred_username: preferred_username} = actor ->
link = "<span class='h-card mention'>@<span>#{preferred_username}</span></span>"
# %Actor{preferred_username: preferred_username} = actor ->
# link = "<span class='h-card mention'>@<span>#{preferred_username}</span></span>"
#
# {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}}
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}}
%Actor{type: :Person, id: id, url: url, preferred_username: preferred_username} = actor ->
%Actor{type: :Person, id: id, preferred_username: preferred_username} = actor ->
link =
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{url}'>@<span>#{
preferred_username
}</span></a></span>"
"<span class='h-card mention' data-user='#{id}'>@<span>#{preferred_username}</span></span>"
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}}

View File

@@ -38,7 +38,8 @@ defmodule Mobilizon.Service.HTML.Scrubber.Default do
"tag",
"nofollow",
"noopener",
"noreferrer"
"noreferrer",
"ugc"
])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
@@ -61,8 +62,8 @@ defmodule Mobilizon.Service.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("ul", [])
Meta.allow_tag_with_these_attributes("img", ["src", "alt"])
Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", [])
Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card", "mention"])
Meta.allow_tag_with_these_attributes("span", ["data-user"])
Meta.allow_tag_with_these_attributes("h1", [])
Meta.allow_tag_with_these_attributes("h2", [])

View File

@@ -15,6 +15,7 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do
require Logger
@spec key_id_to_actor_url(String.t()) :: String.t()
def key_id_to_actor_url(key_id) do
%{path: path} =
uri =
@@ -46,12 +47,10 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do
end
end
@doc """
Gets a public key for a given ActivityPub actor ID (url).
"""
# Gets a public key for a given ActivityPub actor ID (url).
@spec get_public_key_for_url(String.t()) ::
{:ok, String.t()} | {:error, :actor_fetch_error | :pem_decode_error}
def get_public_key_for_url(url) do
defp get_public_key_for_url(url) do
with {:ok, %Actor{keys: keys}} <- ActivityPub.get_or_fetch_actor_by_url(url),
{:ok, public_key} <- prepare_public_key(keys) do
{:ok, public_key}
@@ -103,16 +102,10 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do
end
end
def generate_date_header(date \\ Timex.now("GMT")) do
case Timex.format(date, "%a, %d %b %Y %H:%M:%S %Z", :strftime) do
{:ok, date} ->
date
def generate_date_header, do: generate_date_header(NaiveDateTime.utc_now())
{:error, err} ->
Logger.error("Unable to generate date header")
Logger.debug(inspect(err))
nil
end
def generate_date_header(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end
def generate_request_target(method, path), do: "#{method} #{path}"

View File

@@ -0,0 +1,17 @@
defmodule Mobilizon.Service.Workers.BackgroundWorker do
@moduledoc """
Worker to build search results
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
use Mobilizon.Service.Workers.WorkerHelper, queue: "background"
@impl Oban.Worker
def perform(%{"op" => "delete_actor", "actor_id" => actor_id}, _job) do
with %Actor{} = actor <- Actors.get_actor(actor_id) do
Actors.perform(:delete_actor, actor)
end
end
end