Add admin interface to manage instances subscriptions
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
40
lib/service/activity_pub/converter/tombstone.ex
Normal file
40
lib/service/activity_pub/converter/tombstone.ex
Normal 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
|
||||
@@ -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" ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})}}
|
||||
|
||||
|
||||
@@ -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", [])
|
||||
|
||||
@@ -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}"
|
||||
|
||||
17
lib/service/workers/background_worker.ex
Normal file
17
lib/service/workers/background_worker.ex
Normal 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
|
||||
Reference in New Issue
Block a user