Split Federation as separate context
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Activity do
|
||||
@moduledoc """
|
||||
Represents an activity.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
data: String.t(),
|
||||
local: boolean,
|
||||
actor: Actor.t(),
|
||||
recipients: [String.t()]
|
||||
}
|
||||
|
||||
defstruct [
|
||||
:data,
|
||||
:local,
|
||||
:actor,
|
||||
:recipients
|
||||
]
|
||||
end
|
||||
@@ -1,998 +0,0 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/activity_pub.ex
|
||||
|
||||
defmodule Mobilizon.Service.ActivityPub do
|
||||
@moduledoc """
|
||||
# ActivityPub context.
|
||||
"""
|
||||
|
||||
import Mobilizon.Service.ActivityPub.Utils
|
||||
import Mobilizon.Service.ActivityPub.Visibility
|
||||
|
||||
alias Mobilizon.{Actors, Config, Events, Reports, Users, Share}
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Events.{Comment, Event, Participant}
|
||||
alias Mobilizon.Reports.Report
|
||||
alias Mobilizon.Tombstone
|
||||
alias Mobilizon.Service.ActivityPub.{Activity, Converter, Convertible, Relay, Transmogrifier}
|
||||
alias Mobilizon.Service.{Federator, WebFinger}
|
||||
alias Mobilizon.Service.HTTPSignatures.Signature
|
||||
alias MobilizonWeb.API.Utils, as: APIUtils
|
||||
alias Mobilizon.Service.ActivityPub.Audience
|
||||
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
|
||||
alias MobilizonWeb.Email.{Admin, Mailer}
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Wraps an object into an activity
|
||||
"""
|
||||
@spec create_activity(map(), boolean()) :: {:ok, %Activity{}}
|
||||
def create_activity(map, local \\ true) when is_map(map) do
|
||||
with map <- lazy_put_activity_defaults(map) do
|
||||
{:ok,
|
||||
%Activity{
|
||||
data: map,
|
||||
local: local,
|
||||
actor: map["actor"],
|
||||
recipients: get_recipients(map)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetch an object from an URL, from our local database of events and comments, then eventually remote
|
||||
"""
|
||||
# TODO: Make database calls parallel
|
||||
@spec fetch_object_from_url(String.t()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()}
|
||||
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)},
|
||||
{:existing_actor, {:error, :actor_not_found}} <-
|
||||
{:existing_actor, Actors.get_actor_by_url(url)},
|
||||
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
|
||||
HTTPoison.get(
|
||||
url,
|
||||
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"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["attributedTo"],
|
||||
"object" => data
|
||||
},
|
||||
{:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
|
||||
case data["type"] do
|
||||
"Event" ->
|
||||
{:ok, Events.get_public_event_by_url_with_preload!(object_url)}
|
||||
|
||||
"Note" ->
|
||||
{:ok, Events.get_comment_from_url_with_preload!(object_url)}
|
||||
|
||||
"Actor" ->
|
||||
{:ok, Actors.get_actor_by_url!(object_url, true)}
|
||||
|
||||
other ->
|
||||
{:error, other}
|
||||
end
|
||||
else
|
||||
{:existing_event, %Event{url: event_url}} ->
|
||||
{:ok, Events.get_public_event_by_url_with_preload!(event_url)}
|
||||
|
||||
{:existing_comment, %Comment{url: comment_url}} ->
|
||||
{:ok, Events.get_comment_from_url_with_preload!(comment_url)}
|
||||
|
||||
{: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
|
||||
end
|
||||
|
||||
@doc """
|
||||
Getting an actor from url, eventually creating it
|
||||
"""
|
||||
@spec get_or_fetch_actor_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
|
||||
def get_or_fetch_actor_by_url(url, preload \\ false)
|
||||
|
||||
def get_or_fetch_actor_by_url("https://www.w3.org/ns/activitystreams#Public", _preload) do
|
||||
with %Actor{url: url} <- Relay.get_actor() do
|
||||
get_or_fetch_actor_by_url(url)
|
||||
end
|
||||
end
|
||||
|
||||
def get_or_fetch_actor_by_url(url, preload) do
|
||||
case Actors.get_actor_by_url(url, preload) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor}
|
||||
|
||||
_ ->
|
||||
case make_actor_from_url(url, preload) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor}
|
||||
|
||||
err ->
|
||||
Logger.warn("Could not fetch by AP id")
|
||||
Logger.debug(inspect(err))
|
||||
{:error, "Could not fetch by AP id"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Create`
|
||||
|
||||
* Creates the object, which returns AS data
|
||||
* Wraps ActivityStreams data into a `Create` activity
|
||||
* Creates an `Mobilizon.Service.ActivityPub.Activity` from this
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
|
||||
def create(type, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("creating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:tombstone, nil} <- {:tombstone, check_for_tombstones(args)},
|
||||
{:ok, entity, create_data} <-
|
||||
(case type do
|
||||
:event -> create_event(args, additional)
|
||||
:comment -> create_comment(args, additional)
|
||||
:group -> create_group(args, additional)
|
||||
end),
|
||||
{:ok, activity} <- create_activity(create_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Update`
|
||||
|
||||
* Updates the object, which returns AS data
|
||||
* Wraps ActivityStreams data into a `Update` activity
|
||||
* Creates an `Mobilizon.Service.ActivityPub.Activity` from this
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec update(atom(), struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
|
||||
def update(type, old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("updating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:ok, entity, update_data} <-
|
||||
(case type do
|
||||
:event -> update_event(old_entity, args, additional)
|
||||
:actor -> update_actor(old_entity, args, additional)
|
||||
end),
|
||||
{:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def accept(type, entity, local \\ true, additional \\ %{}) do
|
||||
Logger.debug("We're accepting something")
|
||||
|
||||
{:ok, entity, update_data} =
|
||||
case type do
|
||||
:join -> accept_join(entity, additional)
|
||||
:follow -> accept_follow(entity, additional)
|
||||
end
|
||||
|
||||
with {:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def reject(type, entity, local \\ true, additional \\ %{}) do
|
||||
{:ok, entity, update_data} =
|
||||
case type do
|
||||
:join -> reject_join(entity, additional)
|
||||
:follow -> reject_follow(entity, additional)
|
||||
end
|
||||
|
||||
with {:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def announce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
local \\ true,
|
||||
public \\ true
|
||||
) do
|
||||
with 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 <- maybe_federate(activity) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def unannounce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
cancelled_activity_id \\ nil,
|
||||
local \\ true
|
||||
) do
|
||||
with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
|
||||
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
|
||||
{:ok, unannounce_activity} <- create_activity(unannounce_data, local),
|
||||
:ok <- maybe_federate(unannounce_activity) do
|
||||
{:ok, unannounce_activity, object}
|
||||
else
|
||||
_e -> {:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor follow another
|
||||
"""
|
||||
def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
|
||||
with {:ok, %Follower{} = follower} <-
|
||||
Actors.follow(followed, follower, activity_id, false),
|
||||
follower_as_data <- Convertible.model_to_as(follower),
|
||||
{:ok, activity} <- create_activity(follower_as_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, follower}
|
||||
else
|
||||
{:error, err, msg} when err in [:already_following, :suspended] ->
|
||||
{:error, msg}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor unfollow another
|
||||
"""
|
||||
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
|
||||
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
|
||||
with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower),
|
||||
# We recreate the follow activity
|
||||
follow_as_data <-
|
||||
Convertible.model_to_as(%{follow | actor: follower, target_actor: followed}),
|
||||
{:ok, follow_activity} <- create_activity(follow_as_data, local),
|
||||
activity_unfollow_id <-
|
||||
activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity",
|
||||
unfollow_data <-
|
||||
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id),
|
||||
{:ok, activity} <- create_activity(unfollow_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
err ->
|
||||
Logger.debug("Error while unfollowing an actor #{inspect(err)}")
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def delete(object, local \\ true)
|
||||
|
||||
@spec delete(Event.t(), boolean) :: {:ok, Activity.t(), Event.t()}
|
||||
def delete(%Event{url: url, organizer_actor: actor} = event, local) do
|
||||
data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
"object" => url,
|
||||
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"],
|
||||
"id" => url <> "/delete"
|
||||
}
|
||||
|
||||
with audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(event),
|
||||
{:ok, %Event{} = event} <- Events.delete_event(event),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}),
|
||||
Share.delete_all_by_uri(event.url),
|
||||
{: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",
|
||||
"actor" => actor.url,
|
||||
"object" => url,
|
||||
"id" => url <> "/delete",
|
||||
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
with audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(comment),
|
||||
{:ok, %Comment{} = comment} <- Events.delete_comment(comment),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}),
|
||||
Share.delete_all_by_uri(comment.url),
|
||||
{:ok, activity} <- create_activity(Map.merge(data, audience), local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, comment}
|
||||
end
|
||||
end
|
||||
|
||||
def delete(%Actor{url: url} = actor, local) do
|
||||
data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => url,
|
||||
"object" => url,
|
||||
"id" => url <> "/delete",
|
||||
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor),
|
||||
{:ok, activity} <- create_activity(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, actor}
|
||||
end
|
||||
end
|
||||
|
||||
def flag(args, local \\ false, _additional \\ %{}) do
|
||||
with {:build_args, args} <- {:build_args, prepare_args_for_report(args)},
|
||||
{:create_report, {:ok, %Report{} = report}} <-
|
||||
{:create_report, Reports.create_report(args)},
|
||||
report_as_data <- Convertible.model_to_as(report),
|
||||
cc <- if(local, do: [report.reported.url], else: []),
|
||||
report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}),
|
||||
{:ok, activity} <- create_activity(report_as_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
Enum.each(Users.list_moderators(), fn moderator ->
|
||||
moderator
|
||||
|> Admin.report(report)
|
||||
|> Mailer.deliver_later()
|
||||
end)
|
||||
|
||||
{:ok, activity, report}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def join(object, actor, local \\ true, additional \\ %{})
|
||||
|
||||
def join(%Event{} = event, %Actor{} = actor, local, additional) do
|
||||
# TODO Refactor me for federation
|
||||
with {:maximum_attendee_capacity, true} <-
|
||||
{:maximum_attendee_capacity, check_attendee_capacity(event)},
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.create_participant(%{
|
||||
role: :not_approved,
|
||||
event_id: event.id,
|
||||
actor_id: actor.id,
|
||||
url: Map.get(additional, :url)
|
||||
}),
|
||||
join_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(participant),
|
||||
{:ok, activity} <- create_activity(Map.merge(join_data, audience), local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Implement me
|
||||
def join(%Actor{type: :Group} = _group, %Actor{} = _actor, _local, _additional) do
|
||||
:error
|
||||
end
|
||||
|
||||
defp check_attendee_capacity(%Event{options: options} = event) do
|
||||
with maximum_attendee_capacity <-
|
||||
Map.get(options, :maximum_attendee_capacity) || 0 do
|
||||
maximum_attendee_capacity == 0 ||
|
||||
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
|
||||
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,
|
||||
%Actor{id: actor_id, url: actor_url} = _actor,
|
||||
local
|
||||
) do
|
||||
with {:only_organizer, false} <-
|
||||
{:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)},
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.get_participant(event_id, actor_id),
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Events.delete_participant(participant),
|
||||
leave_data <- %{
|
||||
"type" => "Leave",
|
||||
# If it's an exclusion it should be something else
|
||||
"actor" => actor_url,
|
||||
"object" => event_url,
|
||||
"id" => "#{MobilizonWeb.Endpoint.url()}/leave/event/#{participant.id}"
|
||||
},
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(participant),
|
||||
{:ok, activity} <- create_activity(Map.merge(leave_data, audience), local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, participant}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create an actor locally by its URL (AP ID)
|
||||
"""
|
||||
@spec make_actor_from_url(String.t(), boolean()) :: {:ok, %Actor{}} | {:error, any()}
|
||||
def make_actor_from_url(url, preload \\ false) do
|
||||
case fetch_and_prepare_actor_from_url(url) do
|
||||
{:ok, data} ->
|
||||
Actors.upsert_actor(data, preload)
|
||||
|
||||
# Request returned 410
|
||||
{:error, :actor_deleted} ->
|
||||
Logger.info("Actor was deleted")
|
||||
{:error, :actor_deleted}
|
||||
|
||||
e ->
|
||||
Logger.warn("Failed to make actor from url")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Find an actor in our local database or call WebFinger to find what's its AP ID is and then fetch it
|
||||
"""
|
||||
@spec find_or_make_actor_from_nickname(String.t(), atom() | nil) :: tuple()
|
||||
def find_or_make_actor_from_nickname(nickname, type \\ nil) do
|
||||
case Actors.get_actor_by_name(nickname, type) do
|
||||
%Actor{} = actor ->
|
||||
{:ok, actor}
|
||||
|
||||
nil ->
|
||||
make_actor_from_nickname(nickname)
|
||||
end
|
||||
end
|
||||
|
||||
@spec find_or_make_person_from_nickname(String.t()) :: tuple()
|
||||
def find_or_make_person_from_nickname(nick), do: find_or_make_actor_from_nickname(nick, :Person)
|
||||
|
||||
@spec find_or_make_group_from_nickname(String.t()) :: tuple()
|
||||
def find_or_make_group_from_nickname(nick), do: find_or_make_actor_from_nickname(nick, :Group)
|
||||
|
||||
@doc """
|
||||
Create an actor inside our database from username, using WebFinger to find out its AP ID and then fetch it
|
||||
"""
|
||||
@spec make_actor_from_nickname(String.t()) :: {:ok, %Actor{}} | {:error, any()}
|
||||
def make_actor_from_nickname(nickname) do
|
||||
case WebFinger.finger(nickname) do
|
||||
{:ok, %{"url" => url}} when not is_nil(url) ->
|
||||
make_actor_from_url(url)
|
||||
|
||||
_e ->
|
||||
{:error, "No ActivityPub URL found in WebFinger"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec is_create_activity?(Activity.t()) :: boolean
|
||||
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
|
||||
defp is_create_activity?(_), do: false
|
||||
|
||||
@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_create_activity?(activity) && Config.get([:instance, :allow_relay]) do
|
||||
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
|
||||
|
||||
Relay.publish(activity)
|
||||
end
|
||||
|
||||
followers =
|
||||
if actor.followers_url in activity.recipients do
|
||||
Actors.list_external_followers_for_actor(actor)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
remote_inboxes =
|
||||
(remote_actors(activity) ++ followers)
|
||||
|> Enum.map(fn follower -> follower.shared_inbox_url end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
json = Jason.encode!(data)
|
||||
Logger.debug(fn -> "Remote inboxes are : #{inspect(remote_inboxes)}" end)
|
||||
|
||||
Enum.each(remote_inboxes, fn inbox ->
|
||||
Federator.enqueue(:publish_single_ap, %{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor: actor,
|
||||
id: activity.data["id"]
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Publish an activity to a specific inbox
|
||||
"""
|
||||
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
|
||||
Logger.info("Federating #{id} to #{inbox}")
|
||||
%URI{host: host, path: path} = URI.parse(inbox)
|
||||
|
||||
digest = Signature.build_digest(json)
|
||||
date = Signature.generate_date_header()
|
||||
# request_target = Signature.generate_request_target("POST", path)
|
||||
|
||||
signature =
|
||||
Signature.sign(actor, %{
|
||||
"(request-target)": "post #{path}",
|
||||
host: host,
|
||||
"content-length": byte_size(json),
|
||||
digest: digest,
|
||||
date: date
|
||||
})
|
||||
|
||||
HTTPoison.post(
|
||||
inbox,
|
||||
json,
|
||||
[
|
||||
{"Content-Type", "application/activity+json"},
|
||||
{"signature", signature},
|
||||
{"digest", digest},
|
||||
{"date", date}
|
||||
],
|
||||
hackney: [pool: :default]
|
||||
)
|
||||
end
|
||||
|
||||
# Fetching a remote actor's information through its AP ID
|
||||
@spec fetch_and_prepare_actor_from_url(String.t()) :: {:ok, struct()} | {:error, atom()} | any()
|
||||
defp fetch_and_prepare_actor_from_url(url) do
|
||||
Logger.debug("Fetching and preparing actor from url")
|
||||
Logger.debug(inspect(url))
|
||||
|
||||
res =
|
||||
with %HTTPoison.Response{status_code: 200, body: body} <-
|
||||
HTTPoison.get!(url, [Accept: "application/activity+json"], follow_redirect: true),
|
||||
: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")
|
||||
Mobilizon.Service.ActivityPub.Converter.Actor.as_to_model_data(data)
|
||||
else
|
||||
# Actor is gone, probably deleted
|
||||
{:ok, %HTTPoison.Response{status_code: 410}} ->
|
||||
Logger.info("Response HTTP 410")
|
||||
{:error, :actor_deleted}
|
||||
|
||||
e ->
|
||||
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return all public activities (events & comments) for an actor
|
||||
"""
|
||||
@spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map()
|
||||
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 1, limit \\ 10) do
|
||||
{:ok, events, total_events} = Events.list_public_events_for_actor(actor, page, limit)
|
||||
{:ok, comments, total_comments} = Events.list_public_comments_for_actor(actor, page, limit)
|
||||
|
||||
event_activities = Enum.map(events, &event_to_activity/1)
|
||||
|
||||
comment_activities = Enum.map(comments, &comment_to_activity/1)
|
||||
|
||||
activities = event_activities ++ comment_activities
|
||||
|
||||
%{elements: activities, total: total_events + total_comments}
|
||||
end
|
||||
|
||||
# Create an activity from an event
|
||||
@spec event_to_activity(%Event{}, boolean()) :: Activity.t()
|
||||
defp event_to_activity(%Event{} = event, local \\ true) do
|
||||
%Activity{
|
||||
recipients: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
actor: event.organizer_actor.url,
|
||||
data: Converter.Event.model_to_as(event),
|
||||
local: local
|
||||
}
|
||||
end
|
||||
|
||||
# Create an activity from a comment
|
||||
@spec comment_to_activity(%Comment{}, boolean()) :: Activity.t()
|
||||
defp comment_to_activity(%Comment{} = comment, local \\ true) do
|
||||
%Activity{
|
||||
recipients: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
actor: comment.actor.url,
|
||||
data: Converter.Comment.model_to_as(comment),
|
||||
local: local
|
||||
}
|
||||
end
|
||||
|
||||
# Get recipients for an activity or object
|
||||
@spec get_recipients(map()) :: list()
|
||||
defp get_recipients(data) do
|
||||
(data["to"] || []) ++ (data["cc"] || [])
|
||||
end
|
||||
|
||||
@spec create_event(map(), map()) :: {:ok, map()}
|
||||
defp create_event(args, additional) do
|
||||
with args <- prepare_args_for_event(args),
|
||||
{:ok, %Event{} = event} <- Events.create_event(args),
|
||||
event_as_data <- Convertible.model_to_as(event),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(event),
|
||||
create_data <-
|
||||
make_create_data(event_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, event, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_comment(map(), map()) :: {:ok, map()}
|
||||
defp create_comment(args, additional) do
|
||||
with args <- prepare_args_for_comment(args),
|
||||
{:ok, %Comment{} = comment} <- Events.create_comment(args),
|
||||
comment_as_data <- Convertible.model_to_as(comment),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(comment),
|
||||
create_data <-
|
||||
make_create_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, comment, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_group(map(), map()) :: {:ok, map()}
|
||||
defp create_group(args, additional) do
|
||||
with args <- prepare_args_for_group(args),
|
||||
{:ok, %Actor{type: :Group} = group} <- Actors.create_group(args),
|
||||
group_as_data <- Convertible.model_to_as(group),
|
||||
audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(group_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, group, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_for_tombstones(map()) :: Tombstone.t() | nil
|
||||
defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url)
|
||||
defp check_for_tombstones(_), do: nil
|
||||
|
||||
@spec update_event(Event.t(), map(), map()) ::
|
||||
{:ok, Event.t(), Activity.t()} | any()
|
||||
defp update_event(
|
||||
%Event{} = old_event,
|
||||
args,
|
||||
additional
|
||||
) do
|
||||
with args <- prepare_args_for_event(args),
|
||||
{:ok, %Event{} = new_event} <- Events.update_event(old_event, args),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"),
|
||||
event_as_data <- Convertible.model_to_as(new_event),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(new_event),
|
||||
update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_event, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_actor(Actor.t(), map(), map()) ::
|
||||
{:ok, Actor.t(), Activity.t()} | any()
|
||||
defp update_actor(%Actor{} = old_actor, args, additional) do
|
||||
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
|
||||
actor_as_data <- Convertible.model_to_as(new_actor),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(new_actor),
|
||||
additional <- Map.merge(additional, %{"actor" => old_actor.url}),
|
||||
update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_actor, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept_follow(Follower.t(), map()) ::
|
||||
{:ok, Follower.t(), Activity.t()} | any()
|
||||
defp accept_follow(
|
||||
%Follower{} = follower,
|
||||
additional
|
||||
) do
|
||||
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}),
|
||||
follower_as_data <- Convertible.model_to_as(follower),
|
||||
update_data <-
|
||||
make_accept_join_data(
|
||||
follower_as_data,
|
||||
Map.merge(additional, %{
|
||||
"id" => "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follower.id}",
|
||||
"to" => [follower.actor.url],
|
||||
"cc" => [],
|
||||
"actor" => follower.target_actor.url
|
||||
})
|
||||
) do
|
||||
{:ok, follower, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept_join(Participant.t(), map()) ::
|
||||
{:ok, Participant.t(), Activity.t()} | any()
|
||||
defp accept_join(
|
||||
%Participant{} = participant,
|
||||
additional
|
||||
) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participant, %{role: :participant}),
|
||||
Absinthe.Subscription.publish(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),
|
||||
update_data <-
|
||||
make_accept_join_data(
|
||||
participant_as_data,
|
||||
Map.merge(Map.merge(audience, additional), %{
|
||||
"id" => "#{MobilizonWeb.Endpoint.url()}/accept/join/#{participant.id}"
|
||||
})
|
||||
) do
|
||||
{:ok, participant, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec 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
|
||||
args =
|
||||
if Map.has_key?(args, :title) && !is_nil(args.title),
|
||||
do: Map.update(args, :title, "", &String.trim(HtmlSanitizeEx.strip_tags(&1))),
|
||||
else: args
|
||||
|
||||
# If we've been given a description (we might not get one if updating)
|
||||
# sanitize it, HTML it, and extract tags & mentions from it
|
||||
args =
|
||||
if Map.has_key?(args, :description) && !is_nil(args.description) do
|
||||
{description, mentions, tags} =
|
||||
APIUtils.make_content_html(
|
||||
String.trim(args.description),
|
||||
Map.get(args, :tags, []),
|
||||
"text/html"
|
||||
)
|
||||
|
||||
mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions)
|
||||
|
||||
Map.merge(args, %{
|
||||
description: description,
|
||||
mentions: mentions,
|
||||
tags: tags
|
||||
})
|
||||
else
|
||||
args
|
||||
end
|
||||
|
||||
Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1)
|
||||
end
|
||||
|
||||
# Prepare and sanitize arguments for comments
|
||||
defp prepare_args_for_comment(args) do
|
||||
with in_reply_to_comment <-
|
||||
args |> Map.get(:in_reply_to_comment_id) |> 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(
|
||||
args |> Map.get(:text, "") |> String.trim(),
|
||||
# Can't put additional tags on a comment
|
||||
[],
|
||||
"text/html"
|
||||
),
|
||||
tags <- ConverterUtils.fetch_tags(tags),
|
||||
mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions),
|
||||
args <-
|
||||
Map.merge(args, %{
|
||||
actor_id: Map.get(args, :actor_id),
|
||||
text: text,
|
||||
mentions: mentions,
|
||||
tags: tags,
|
||||
event: event,
|
||||
in_reply_to_comment: in_reply_to_comment,
|
||||
in_reply_to_comment_id:
|
||||
if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id)),
|
||||
origin_comment_id:
|
||||
if(is_nil(in_reply_to_comment),
|
||||
do: nil,
|
||||
else: Comment.get_thread_id(in_reply_to_comment)
|
||||
)
|
||||
}) do
|
||||
args
|
||||
end
|
||||
end
|
||||
|
||||
@spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil
|
||||
defp handle_event_for_comment(event_id) when not is_nil(event_id) do
|
||||
case Events.get_event_with_preload(event_id) do
|
||||
{:ok, %Event{} = event} -> event
|
||||
{:error, :event_not_found} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_event_for_comment(nil), do: nil
|
||||
|
||||
defp prepare_args_for_group(args) do
|
||||
with preferred_username <-
|
||||
args |> Map.get(:preferred_username) |> HtmlSanitizeEx.strip_tags() |> String.trim(),
|
||||
summary <- args |> Map.get(:summary, "") |> String.trim(),
|
||||
{summary, _mentions, _tags} <-
|
||||
summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do
|
||||
%{args | preferred_username: preferred_username, summary: summary}
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_args_for_report(args) do
|
||||
with {:reporter, %Actor{} = reporter_actor} <-
|
||||
{:reporter, Actors.get_actor!(args.reporter_id)},
|
||||
{:reported, %Actor{} = reported_actor} <-
|
||||
{:reported, Actors.get_actor!(args.reported_id)},
|
||||
content <- HtmlSanitizeEx.strip_tags(args.content),
|
||||
event <- Events.get_comment(Map.get(args, :event_id)),
|
||||
{:get_report_comments, comments} <-
|
||||
{:get_report_comments,
|
||||
Events.list_comments_by_actor_and_ids(
|
||||
reported_actor.id,
|
||||
Map.get(args, :comments_ids, [])
|
||||
)} do
|
||||
Map.merge(args, %{
|
||||
reporter: reporter_actor,
|
||||
reported: reported_actor,
|
||||
content: content,
|
||||
event: event,
|
||||
comments: comments
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,185 +0,0 @@
|
||||
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"
|
||||
|
||||
@doc """
|
||||
Determines the full audience based on mentions for a public audience
|
||||
|
||||
Audience is:
|
||||
* `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(), String.t()) :: {list(), list()}
|
||||
def get_to_and_cc(%Actor{} = actor, mentions, :public) do
|
||||
to = [@ap_public | mentions]
|
||||
cc = [actor.followers_url]
|
||||
|
||||
{to, cc}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines the full audience based on mentions based on a unlisted audience
|
||||
|
||||
Audience is:
|
||||
* `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(), String.t()) :: {list(), list()}
|
||||
def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do
|
||||
to = [actor.followers_url | mentions]
|
||||
cc = [@ap_public]
|
||||
|
||||
{to, cc}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines the full audience based on mentions based on a private audience
|
||||
|
||||
Audience is:
|
||||
* `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(), 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
|
||||
|
||||
@doc """
|
||||
Determines the full audience based on mentions based on a direct audience
|
||||
|
||||
Audience is:
|
||||
* `to` : the mentioned actors and the eventual actor we're replying to
|
||||
* `cc` : none
|
||||
"""
|
||||
@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, {:list, _}) do
|
||||
{mentions, []}
|
||||
end
|
||||
|
||||
# def get_addressed_actors(_, to) when is_list(to) do
|
||||
# Actors.get(to)
|
||||
# end
|
||||
|
||||
def get_addressed_actors(mentioned_users, _), do: mentioned_users
|
||||
|
||||
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(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
|
||||
@@ -1,11 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converter do
|
||||
@moduledoc """
|
||||
Converter behaviour.
|
||||
|
||||
This module allows to convert from ActivityStream format to our own internal
|
||||
one, and back.
|
||||
"""
|
||||
|
||||
@callback as_to_model_data(map) :: map
|
||||
@callback model_to_as(struct) :: map
|
||||
end
|
||||
@@ -1,114 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converter.Actor do
|
||||
@moduledoc """
|
||||
Actor converter.
|
||||
|
||||
This module allows to convert events from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor, as: ActorModel
|
||||
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils}
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: ActorModel do
|
||||
alias Mobilizon.Service.ActivityPub.Converter.Actor, as: ActorConverter
|
||||
|
||||
defdelegate model_to_as(actor), to: ActorConverter
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: map
|
||||
def as_to_model_data(data) do
|
||||
avatar =
|
||||
data["icon"]["url"] &&
|
||||
%{
|
||||
"name" => data["icon"]["name"] || "avatar",
|
||||
"url" => MobilizonWeb.MediaProxy.url(data["icon"]["url"])
|
||||
}
|
||||
|
||||
banner =
|
||||
data["image"]["url"] &&
|
||||
%{
|
||||
"name" => data["image"]["name"] || "banner",
|
||||
"url" => MobilizonWeb.MediaProxy.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 """
|
||||
Convert an actor struct to an ActivityStream representation.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(ActorModel.t()) :: map
|
||||
def model_to_as(%ActorModel{} = actor) do
|
||||
actor_data = %{
|
||||
"id" => actor.url,
|
||||
"type" => actor.type,
|
||||
"preferredUsername" => actor.preferred_username,
|
||||
"name" => actor.name,
|
||||
"summary" => actor.summary,
|
||||
"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
|
||||
@@ -1,78 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converter.Address do
|
||||
@moduledoc """
|
||||
Address converter.
|
||||
|
||||
This module allows to convert reports from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Addresses.Address, as: AddressModel
|
||||
alias Mobilizon.Service.ActivityPub.Converter
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
@doc """
|
||||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: map
|
||||
def as_to_model_data(object) do
|
||||
res = %{
|
||||
"description" => object["name"],
|
||||
"url" => object["url"]
|
||||
}
|
||||
|
||||
res =
|
||||
if is_nil(object["address"]) do
|
||||
res
|
||||
else
|
||||
Map.merge(res, %{
|
||||
"country" => object["address"]["addressCountry"],
|
||||
"postal_code" => object["address"]["postalCode"],
|
||||
"region" => object["address"]["addressRegion"],
|
||||
"street" => object["address"]["streetAddress"],
|
||||
"locality" => object["address"]["addressLocality"]
|
||||
})
|
||||
end
|
||||
|
||||
if is_nil(object["latitude"]) or is_nil(object["longitude"]) do
|
||||
res
|
||||
else
|
||||
geo = %Geo.Point{
|
||||
coordinates: {object["latitude"], object["longitude"]},
|
||||
srid: 4326
|
||||
}
|
||||
|
||||
Map.put(res, "geom", geo)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert an event struct to an ActivityStream representation.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(AddressModel.t()) :: map
|
||||
def model_to_as(%AddressModel{} = 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
|
||||
res
|
||||
|> Map.put("latitude", address.geom.coordinates |> elem(0))
|
||||
|> Map.put("longitude", address.geom.coordinates |> elem(1))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,148 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
|
||||
@moduledoc """
|
||||
Comment converter.
|
||||
|
||||
This module allows to convert events from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Comment, as: CommentModel
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Visibility}
|
||||
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Tombstone, as: TombstoneModel
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: CommentModel do
|
||||
alias Mobilizon.Service.ActivityPub.Converter.Comment, as: CommentConverter
|
||||
|
||||
defdelegate model_to_as(comment), to: CommentConverter
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
|
||||
def as_to_model_data(object) do
|
||||
Logger.debug("We're converting raw ActivityStream data to a comment entity")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
||||
{:ok, %Actor{id: actor_id, domain: domain}} <-
|
||||
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))
|
||||
|
||||
data = %{
|
||||
text: object["content"],
|
||||
url: object["id"],
|
||||
actor_id: actor_id,
|
||||
in_reply_to_comment_id: nil,
|
||||
event_id: nil,
|
||||
uuid: object["uuid"],
|
||||
tags: tags,
|
||||
mentions: mentions,
|
||||
local: is_nil(domain),
|
||||
visibility: if(Visibility.is_public?(object), do: :public, else: :private)
|
||||
}
|
||||
|
||||
# We fetch the parent object
|
||||
Logger.debug("We're fetching the parent object")
|
||||
|
||||
data =
|
||||
if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
|
||||
object["inReplyTo"] != "" do
|
||||
Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
|
||||
|
||||
case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
|
||||
# Reply to an event (Event)
|
||||
{:ok, %Event{id: id}} ->
|
||||
Logger.debug("Parent object is an event")
|
||||
data |> Map.put(:event_id, id)
|
||||
|
||||
# Reply to a comment (Comment)
|
||||
{:ok, %CommentModel{id: id} = comment} ->
|
||||
Logger.debug("Parent object is another comment")
|
||||
|
||||
data
|
||||
|> Map.put(:in_reply_to_comment_id, id)
|
||||
|> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
|
||||
|> Map.put(:event_id, comment.event_id)
|
||||
|
||||
# Anything else is kind of a MP
|
||||
{:error, parent} ->
|
||||
Logger.warn("Parent object is something we don't handle")
|
||||
Logger.debug(inspect(parent))
|
||||
data
|
||||
end
|
||||
else
|
||||
Logger.debug("No parent object for this comment")
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
{:ok, data}
|
||||
else
|
||||
err ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an AS comment object from an existing `Comment` structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(CommentModel.t()) :: map
|
||||
def model_to_as(%CommentModel{deleted_at: nil} = comment) do
|
||||
to =
|
||||
if comment.visibility == :public,
|
||||
do: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
else: [comment.actor.followers_url]
|
||||
|
||||
object = %{
|
||||
"type" => "Note",
|
||||
"to" => to,
|
||||
"cc" => [],
|
||||
"content" => comment.text,
|
||||
"mediaType" => "text/html",
|
||||
"actor" => comment.actor.url,
|
||||
"attributedTo" => comment.actor.url,
|
||||
"uuid" => comment.uuid,
|
||||
"id" => comment.url,
|
||||
"tag" =>
|
||||
ConverterUtils.build_mentions(comment.mentions) ++ ConverterUtils.build_tags(comment.tags)
|
||||
}
|
||||
|
||||
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
|
||||
Convertible.model_to_as(%TombstoneModel{
|
||||
uri: comment.url,
|
||||
inserted_at: comment.deleted_at
|
||||
})
|
||||
end
|
||||
end
|
||||
@@ -1,198 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converter.Event do
|
||||
@moduledoc """
|
||||
Event converter.
|
||||
|
||||
This module allows to convert events from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Addresses
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Events.Event, as: EventModel
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
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
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: EventModel do
|
||||
alias Mobilizon.Service.ActivityPub.Converter.Event, as: EventConverter
|
||||
|
||||
defdelegate model_to_as(event), to: EventConverter
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map()} | {:error, any()}
|
||||
def as_to_model_data(object) do
|
||||
Logger.debug("event as_to_model_data")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
||||
{:actor, {:ok, %Actor{id: actor_id, domain: actor_domain}}} <-
|
||||
{: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,
|
||||
{:ok, %Picture{id: picture_id}} <-
|
||||
object["attachment"]
|
||||
|> hd
|
||||
|> PictureConverter.find_or_create_picture(actor_id) do
|
||||
picture_id
|
||||
else
|
||||
_err ->
|
||||
nil
|
||||
end
|
||||
|
||||
entity = %{
|
||||
title: object["name"],
|
||||
description: object["content"],
|
||||
organizer_actor_id: actor_id,
|
||||
picture_id: picture_id,
|
||||
begins_on: object["startTime"],
|
||||
ends_on: object["endTime"],
|
||||
category: object["category"],
|
||||
visibility: visibility,
|
||||
join_options: Map.get(object, "joinMode", "free"),
|
||||
local: is_nil(actor_domain),
|
||||
options: options,
|
||||
status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(),
|
||||
online_address: object["onlineAddress"],
|
||||
phone_address: object["phoneAddress"],
|
||||
draft: false,
|
||||
url: object["id"],
|
||||
uuid: object["uuid"],
|
||||
tags: tags,
|
||||
mentions: mentions,
|
||||
physical_address_id: address_id,
|
||||
updated_at: object["updated"],
|
||||
publish_at: object["published"]
|
||||
}
|
||||
|
||||
{:ok, entity}
|
||||
else
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert an event struct to an ActivityStream representation.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(EventModel.t()) :: map
|
||||
def model_to_as(%EventModel{} = event) do
|
||||
to =
|
||||
if event.visibility == :public,
|
||||
do: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
else: [event.organizer_actor.followers_url]
|
||||
|
||||
res = %{
|
||||
"type" => "Event",
|
||||
"to" => to,
|
||||
"cc" => [],
|
||||
"attributedTo" => event.organizer_actor.url,
|
||||
"name" => event.title,
|
||||
"actor" => event.organizer_actor.url,
|
||||
"uuid" => event.uuid,
|
||||
"category" => event.category,
|
||||
"content" => event.description,
|
||||
"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(),
|
||||
"joinMode" => to_string(event.join_options),
|
||||
"endTime" => event.ends_on |> date_to_string(),
|
||||
"tag" => event.tags |> ConverterUtils.build_tags(),
|
||||
"maximumAttendeeCapacity" => event.options.maximum_attendee_capacity,
|
||||
"repliesModerationOption" => event.options.comment_moderation,
|
||||
"commentsEnabled" => event.options.comment_moderation == :allow_all,
|
||||
# "draft" => event.draft,
|
||||
"ical:status" => event.status |> to_string |> String.upcase(),
|
||||
"id" => event.url,
|
||||
"url" => event.url
|
||||
}
|
||||
|
||||
res =
|
||||
if is_nil(event.physical_address),
|
||||
do: res,
|
||||
else: Map.put(res, "location", AddressConverter.model_to_as(event.physical_address))
|
||||
|
||||
if is_nil(event.picture),
|
||||
do: res,
|
||||
else: Map.put(res, "attachment", [PictureConverter.model_to_as(event.picture)])
|
||||
end
|
||||
|
||||
# Get only elements that we have in EventOptions
|
||||
@spec get_options(map) :: map
|
||||
defp get_options(object) do
|
||||
%{
|
||||
maximum_attendee_capacity: object["maximumAttendeeCapacity"],
|
||||
comment_moderation:
|
||||
Map.get(
|
||||
object,
|
||||
"repliesModerationOption",
|
||||
if(Map.get(object, "commentsEnabled", true), do: :allow_all, else: :closed)
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
@spec get_address(map | binary | nil) :: integer | nil
|
||||
defp get_address(address_url) when is_bitstring(address_url) do
|
||||
get_address(%{"id" => address_url})
|
||||
end
|
||||
|
||||
defp get_address(%{"id" => url} = map) when is_map(map) and is_binary(url) do
|
||||
Logger.debug("Address with an URL, let's check against our own database")
|
||||
|
||||
case Addresses.get_address_by_url(url) do
|
||||
%Address{id: address_id} ->
|
||||
address_id
|
||||
|
||||
_ ->
|
||||
Logger.debug("not in our database, let's try to create it")
|
||||
map = Map.put(map, "url", map["id"])
|
||||
do_get_address(map)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_address(map) when is_map(map) do
|
||||
do_get_address(map)
|
||||
end
|
||||
|
||||
defp get_address(nil), do: nil
|
||||
|
||||
@spec do_get_address(map) :: integer | nil
|
||||
defp do_get_address(map) do
|
||||
map = Mobilizon.Service.ActivityPub.Converter.Address.as_to_model_data(map)
|
||||
|
||||
case Addresses.create_address(map) do
|
||||
{:ok, %Address{id: address_id}} ->
|
||||
address_id
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@ap_public "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
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
|
||||
defp date_to_string(%DateTime{} = date), do: DateTime.to_iso8601(date)
|
||||
end
|
||||
@@ -1,105 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converter.Flag do
|
||||
@moduledoc """
|
||||
Flag converter.
|
||||
|
||||
This module allows to convert reports from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
|
||||
Note: Reports are named Flag in AS.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Reports.Report
|
||||
alias Mobilizon.Service.ActivityPub.Converter
|
||||
alias Mobilizon.Service.ActivityPub.Convertible
|
||||
alias Mobilizon.Service.ActivityPub.Relay
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: Report do
|
||||
alias Mobilizon.Service.ActivityPub.Converter.Flag, as: FlagConverter
|
||||
|
||||
defdelegate model_to_as(report), to: FlagConverter
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: map
|
||||
def as_to_model_data(object) do
|
||||
with params <- as_to_model(object) do
|
||||
%{
|
||||
"reporter_id" => params["reporter"].id,
|
||||
"uri" => params["uri"],
|
||||
"content" => params["content"],
|
||||
"reported_id" => params["reported"].id,
|
||||
"event_id" => (!is_nil(params["event"]) && params["event"].id) || nil,
|
||||
"comments" => params["comments"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert an event struct to an ActivityStream representation
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(Report.t()) :: map
|
||||
def model_to_as(%Report{} = report) do
|
||||
object = [report.reported.url] ++ Enum.map(report.comments, fn comment -> comment.url end)
|
||||
|
||||
object = if report.event, do: object ++ [report.event.url], else: object
|
||||
|
||||
%{
|
||||
"type" => "Flag",
|
||||
"actor" => Relay.get_actor().url,
|
||||
"id" => report.url,
|
||||
"content" => report.content,
|
||||
"object" => object
|
||||
}
|
||||
end
|
||||
|
||||
@spec as_to_model(map) :: map
|
||||
def as_to_model(%{"object" => objects} = object) do
|
||||
with {:ok, %Actor{} = reporter} <- Actors.get_actor_by_url(object["actor"]),
|
||||
%Actor{} = reported <-
|
||||
Enum.reduce_while(objects, nil, fn url, _ ->
|
||||
case Actors.get_actor_by_url(url) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:halt, actor}
|
||||
|
||||
_ ->
|
||||
{:cont, nil}
|
||||
end
|
||||
end),
|
||||
event <-
|
||||
Enum.reduce_while(objects, nil, fn url, _ ->
|
||||
case Events.get_event_by_url(url) do
|
||||
%Event{} = event ->
|
||||
{:halt, event}
|
||||
|
||||
_ ->
|
||||
{:cont, nil}
|
||||
end
|
||||
end),
|
||||
|
||||
# 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))
|
||||
end),
|
||||
comments <- Enum.map(comments, &Events.get_comment_from_url/1) do
|
||||
%{
|
||||
"reporter" => reporter,
|
||||
"uri" => object["id"],
|
||||
"content" => object["content"],
|
||||
"reported" => reported,
|
||||
"event" => event,
|
||||
"comments" => comments
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,36 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converter.Follower do
|
||||
@moduledoc """
|
||||
Participant converter.
|
||||
|
||||
This module allows to convert followers from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Follower, as: FollowerModel
|
||||
|
||||
alias Mobilizon.Service.ActivityPub.Convertible
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
defimpl Convertible, for: FollowerModel do
|
||||
alias Mobilizon.Service.ActivityPub.Converter.Follower, as: FollowerConverter
|
||||
|
||||
defdelegate model_to_as(follower), to: FollowerConverter
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert an follow struct to an ActivityStream representation.
|
||||
"""
|
||||
@spec model_to_as(FollowerModel.t()) :: map
|
||||
def model_to_as(
|
||||
%FollowerModel{actor: %Actor{} = actor, target_actor: %Actor{} = target_actor} = follower
|
||||
) do
|
||||
%{
|
||||
"type" => "Follow",
|
||||
"actor" => actor.url,
|
||||
"to" => [target_actor.url],
|
||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"object" => target_actor.url,
|
||||
"id" => follower.url
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -1,31 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converter.Participant do
|
||||
@moduledoc """
|
||||
Participant converter.
|
||||
|
||||
This module allows to convert reports from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Events.Participant, as: ParticipantModel
|
||||
|
||||
alias Mobilizon.Service.ActivityPub.Convertible
|
||||
|
||||
defimpl Convertible, for: ParticipantModel do
|
||||
alias Mobilizon.Service.ActivityPub.Converter.Participant, as: ParticipantConverter
|
||||
|
||||
defdelegate model_to_as(participant), to: ParticipantConverter
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert an event struct to an ActivityStream representation.
|
||||
"""
|
||||
@spec model_to_as(ParticipantModel.t()) :: map
|
||||
def model_to_as(%ParticipantModel{} = participant) do
|
||||
%{
|
||||
"type" => "Join",
|
||||
"id" => participant.url,
|
||||
"actor" => participant.actor.url,
|
||||
"object" => participant.event.url
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converter.Picture do
|
||||
@moduledoc """
|
||||
Picture converter.
|
||||
|
||||
This module allows to convert events from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Media.Picture, as: PictureModel
|
||||
|
||||
@doc """
|
||||
Convert a picture struct to an ActivityStream representation.
|
||||
"""
|
||||
@spec model_to_as(PictureModel.t()) :: map
|
||||
def model_to_as(%PictureModel{file: file}) do
|
||||
%{
|
||||
"type" => "Document",
|
||||
"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
|
||||
@@ -1,40 +0,0 @@
|
||||
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
|
||||
@@ -1,112 +0,0 @@
|
||||
defmodule Mobilizon.Service.ActivityPub.Converter.Utils do
|
||||
@moduledoc """
|
||||
Various utils for converters
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.Tag
|
||||
alias Mobilizon.Mention
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Storage.Repo
|
||||
require Logger
|
||||
|
||||
@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
|
||||
|
||||
@spec fetch_mentions([map()]) :: [map()]
|
||||
def fetch_mentions(mentions) when is_list(mentions) do
|
||||
Logger.debug("fetching mentions")
|
||||
|
||||
Enum.reduce(mentions, [], fn mention, acc -> create_mention(mention, acc) end)
|
||||
end
|
||||
|
||||
def fetch_address(%{id: id}) do
|
||||
with {id, ""} <- Integer.parse(id) do
|
||||
%{id: id}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_address(address) when is_map(address) do
|
||||
address
|
||||
end
|
||||
|
||||
@spec build_tags([Tag.t()]) :: [Map.t()]
|
||||
def build_tags(tags) do
|
||||
Enum.map(tags, fn %Tag{} = tag ->
|
||||
%{
|
||||
"href" => MobilizonWeb.Endpoint.url() <> "/tags/#{tag.slug}",
|
||||
"name" => "##{tag.title}",
|
||||
"type" => "Hashtag"
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
def build_mentions(mentions) do
|
||||
Enum.map(mentions, fn %Mention{} = mention ->
|
||||
if Ecto.assoc_loaded?(mention.actor) do
|
||||
build_mention(mention.actor)
|
||||
else
|
||||
build_mention(Repo.preload(mention, [:actor]).actor)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_mention(%Actor{} = actor) do
|
||||
%{
|
||||
"href" => actor.url,
|
||||
"name" => "@#{Mobilizon.Actors.Actor.preferred_username_and_domain(actor)}",
|
||||
"type" => "Mention"
|
||||
}
|
||||
end
|
||||
|
||||
defp fetch_tag(%{title: title}), do: [title]
|
||||
|
||||
defp fetch_tag(tag) when is_map(tag) do
|
||||
case tag["type"] do
|
||||
"Hashtag" ->
|
||||
[tag_without_hash(tag["name"])]
|
||||
|
||||
_err ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_tag(tag) when is_bitstring(tag), do: [tag_without_hash(tag)]
|
||||
|
||||
defp tag_without_hash("#" <> tag_title), do: tag_title
|
||||
defp tag_without_hash(tag_title), do: tag_title
|
||||
|
||||
defp existing_tag_or_data(tag_title) do
|
||||
case Events.get_tag_by_title(tag_title) do
|
||||
%Tag{} = tag -> %{title: tag.title, id: tag.id}
|
||||
nil -> %{title: tag_title}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_mention(map(), list()) :: list()
|
||||
defp create_mention(%Actor{id: actor_id} = _mention, acc) do
|
||||
acc ++ [%{actor_id: actor_id}]
|
||||
end
|
||||
|
||||
@spec create_mention(map(), list()) :: list()
|
||||
defp create_mention(mention, acc) when is_map(mention) do
|
||||
with true <- mention["type"] == "Mention",
|
||||
{:ok, %Actor{id: actor_id}} <- ActivityPub.get_or_fetch_actor_by_url(mention["href"]) do
|
||||
acc ++ [%{actor_id: actor_id}]
|
||||
else
|
||||
_err ->
|
||||
acc
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_mention({String.t(), map()}, list()) :: list()
|
||||
defp create_mention({_, mention}, acc) when is_map(mention) do
|
||||
create_mention(mention, acc)
|
||||
end
|
||||
end
|
||||
@@ -1,10 +0,0 @@
|
||||
defprotocol Mobilizon.Service.ActivityPub.Convertible do
|
||||
@moduledoc """
|
||||
Convertible protocol.
|
||||
"""
|
||||
|
||||
@type activity_streams :: map
|
||||
|
||||
@spec model_to_as(t) :: activity_streams
|
||||
def model_to_as(convertible)
|
||||
end
|
||||
@@ -1,155 +0,0 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/relay.ex
|
||||
|
||||
defmodule Mobilizon.Service.ActivityPub.Relay do
|
||||
@moduledoc """
|
||||
Handles following and unfollowing relays and instances.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
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_instance_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
|
||||
actor
|
||||
end
|
||||
end
|
||||
|
||||
@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, follow} <- Follows.follow(local_actor, target_actor) do
|
||||
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
e ->
|
||||
Logger.warn("Error while following remote instance: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@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, follow} <- Follows.unfollow(local_actor, target_actor) do
|
||||
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
e ->
|
||||
Logger.warn("Error while unfollowing remote instance: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@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, follow} <- Follows.accept(target_actor, local_actor) do
|
||||
{:ok, activity, follow}
|
||||
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(),
|
||||
{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)}")
|
||||
end
|
||||
end
|
||||
|
||||
def publish(err) do
|
||||
Logger.error("Tried to publish a bad activity")
|
||||
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
|
||||
@@ -1,565 +0,0 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/transmogrifier.ex
|
||||
|
||||
defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
@moduledoc """
|
||||
A module to handle coding from internal to wire ActivityPub and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Comment, Event, Participant}
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.ActivityPub.{Activity, Converter, Convertible, Utils}
|
||||
alias MobilizonWeb.Email.Participation
|
||||
|
||||
import Mobilizon.Service.ActivityPub.Utils
|
||||
|
||||
require Logger
|
||||
|
||||
def handle_incoming(%{"id" => nil}), do: :error
|
||||
def handle_incoming(%{"id" => ""}), do: :error
|
||||
|
||||
def handle_incoming(%{"type" => "Flag"} = data) do
|
||||
with params <- Converter.Flag.as_to_model(data) do
|
||||
params = %{
|
||||
reporter_id: params["reporter"].id,
|
||||
reported_id: params["reported"].id,
|
||||
comments_ids: params["comments"] |> Enum.map(& &1.id),
|
||||
content: params["content"] || "",
|
||||
additional: %{
|
||||
"cc" => [params["reported"].url]
|
||||
},
|
||||
event_id: if(is_nil(params["event"]), do: nil, else: params["event"].id || nil),
|
||||
local: false
|
||||
}
|
||||
|
||||
ActivityPub.flag(params, false)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles a `Create` activity for `Note` (comments) objects
|
||||
|
||||
The following actions are performed
|
||||
* Fetch the author of the activity
|
||||
* Convert the ActivityStream data to the comment model format (it also finds and inserts tags)
|
||||
* Get (by it's URL) or create the comment with this data
|
||||
* Insert eventual mentions in the database
|
||||
* Convert the comment back in ActivityStreams data
|
||||
* Wrap this data back into a `Create` activity
|
||||
* Return the activity and the comment object
|
||||
"""
|
||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object}) do
|
||||
Logger.info("Handle incoming to create notes")
|
||||
|
||||
with {:ok, object_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} <-
|
||||
ActivityPub.create(:comment, object_data, false) do
|
||||
{:ok, activity, comment}
|
||||
else
|
||||
{:existing_comment, {:ok, %Comment{} = comment}} ->
|
||||
{:ok, nil, comment}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles a `Create` activity for `Event` objects
|
||||
|
||||
The following actions are performed
|
||||
* Fetch the author of the activity
|
||||
* Convert the ActivityStream data to the event model format (it also finds and inserts tags)
|
||||
* Get (by it's URL) or create the event with this data
|
||||
* Insert eventual mentions in the database
|
||||
* Convert the event back in ActivityStreams data
|
||||
* Wrap this data back into a `Create` activity
|
||||
* Return the activity and the event object
|
||||
"""
|
||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object}) do
|
||||
Logger.info("Handle incoming to create event")
|
||||
|
||||
with {:ok, object_data} <-
|
||||
object |> Converter.Event.as_to_model_data(),
|
||||
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Event{} = event} <-
|
||||
ActivityPub.create(:event, object_data, false) do
|
||||
{:ok, activity, event}
|
||||
else
|
||||
{:existing_event, %Event{} = event} -> {:ok, nil, event}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
|
||||
) do
|
||||
with {:ok, %Actor{} = followed} <- ActivityPub.get_or_fetch_actor_by_url(followed, true),
|
||||
{:ok, %Actor{} = follower} <- ActivityPub.get_or_fetch_actor_by_url(follower),
|
||||
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
Logger.warn("Unable to handle Follow activity #{inspect(e)}")
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Accept",
|
||||
"object" => accepted_object,
|
||||
"actor" => _actor,
|
||||
"id" => id
|
||||
} = data
|
||||
) do
|
||||
with actor_url <- get_actor(data),
|
||||
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
|
||||
{:object_not_found, {:ok, %Activity{} = activity, object}} <-
|
||||
{:object_not_found,
|
||||
do_handle_incoming_accept_following(accepted_object, actor) ||
|
||||
do_handle_incoming_accept_join(accepted_object, actor)} do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{:object_not_found, nil} ->
|
||||
Logger.warn(
|
||||
"Unable to process Accept activity #{inspect(id)}. Object #{inspect(accepted_object)} wasn't found."
|
||||
)
|
||||
|
||||
:error
|
||||
|
||||
e ->
|
||||
Logger.warn(
|
||||
"Unable to process Accept activity #{inspect(id)} for object #{inspect(accepted_object)} only returned #{
|
||||
inspect(e)
|
||||
}"
|
||||
)
|
||||
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Reject", "object" => rejected_object, "actor" => _actor, "id" => id} = data
|
||||
) do
|
||||
with actor_url <- get_actor(data),
|
||||
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
|
||||
{:object_not_found, {:ok, activity, object}} <-
|
||||
{:object_not_found,
|
||||
do_handle_incoming_reject_following(rejected_object, actor) ||
|
||||
do_handle_incoming_reject_join(rejected_object, actor)} do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{:object_not_found, nil} ->
|
||||
Logger.warn(
|
||||
"Unable to process Reject activity #{inspect(id)}. Object #{inspect(rejected_object)} wasn't found."
|
||||
)
|
||||
|
||||
:error
|
||||
|
||||
e ->
|
||||
Logger.warn(
|
||||
"Unable to process Reject activity #{inspect(id)} for object #{inspect(rejected_object)} only returned #{
|
||||
inspect(e)
|
||||
}"
|
||||
)
|
||||
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Announce", "object" => object, "actor" => _actor, "id" => _id} = data
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
# TODO: Is the following line useful?
|
||||
{:ok, %Actor{id: actor_id} = _actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
|
||||
:ok <- Logger.debug("Fetching contained object"),
|
||||
{:ok, object} <- fetch_obj_helper_as_activity_streams(object),
|
||||
:ok <- Logger.debug("Handling contained object"),
|
||||
create_data <-
|
||||
make_create_data(object),
|
||||
:ok <- Logger.debug(inspect(object)),
|
||||
{:ok, _activity, entity} <- handle_incoming(create_data),
|
||||
:ok <- Logger.debug("Finished processing contained object"),
|
||||
{:ok, activity} <- ActivityPub.create_activity(data, false),
|
||||
{:ok, %Actor{id: object_owner_actor_id}} <- Actors.get_actor_by_url(object["actor"]),
|
||||
{:ok, %Mobilizon.Share{} = _share} <-
|
||||
Mobilizon.Share.create(object["id"], actor_id, object_owner_actor_id) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
e ->
|
||||
Logger.debug(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(%{
|
||||
"type" => "Update",
|
||||
"object" => %{"type" => object_type} = object,
|
||||
"actor" => _actor_id
|
||||
})
|
||||
when object_type in ["Person", "Group", "Application", "Service", "Organization"] do
|
||||
with {:ok, %Actor{} = old_actor} <- Actors.get_actor_by_url(object["id"]),
|
||||
{:ok, object_data} <-
|
||||
object |> Converter.Actor.as_to_model_data(),
|
||||
{:ok, %Activity{} = activity, %Actor{} = new_actor} <-
|
||||
ActivityPub.update(:actor, old_actor, object_data, false) do
|
||||
{:ok, activity, new_actor}
|
||||
else
|
||||
e ->
|
||||
Logger.debug(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => _actor} =
|
||||
update_data
|
||||
) do
|
||||
with actor <- 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 |> 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}
|
||||
else
|
||||
_e ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{
|
||||
"type" => "Announce",
|
||||
"object" => object_id,
|
||||
"id" => cancelled_activity_id
|
||||
},
|
||||
"actor" => _actor,
|
||||
"id" => id
|
||||
} = data
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
|
||||
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
|
||||
{:ok, activity, object} <-
|
||||
ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => "Follow", "object" => followed},
|
||||
"actor" => follower,
|
||||
"id" => id
|
||||
} = _data
|
||||
) do
|
||||
with {:ok, %Actor{domain: nil} = followed} <- Actors.get_actor_by_url(followed),
|
||||
{:ok, %Actor{} = follower} <- Actors.get_actor_by_url(follower),
|
||||
{:ok, activity, object} <- ActivityPub.unfollow(follower, followed, id, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
Logger.debug(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: We presently assume that any actor on the same origin domain as the object being
|
||||
# deleted has the rights to delete that object. A better way to validate whether or not
|
||||
# the object should be deleted is to refetch the object URI, which should return either
|
||||
# an error or a tombstone. This would allow us to verify that a deletion actually took
|
||||
# place.
|
||||
def handle_incoming(
|
||||
%{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
{:ok, %Actor{url: actor_url}} <- Actors.get_actor_by_url(actor),
|
||||
object_id <- Utils.get_url(object),
|
||||
{:origin_check, true} <- {:origin_check, origin_check_from_id?(actor_url, object_id)},
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object_id),
|
||||
{:ok, activity, object} <- ActivityPub.delete(object, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{:origin_check, false} ->
|
||||
Logger.warn("Object origin check failed")
|
||||
:error
|
||||
|
||||
e ->
|
||||
Logger.debug(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Join", "object" => object, "actor" => _actor, "id" => id} = data
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
{:ok, %Actor{url: _actor_url} = actor} <- Actors.get_actor_by_url(actor),
|
||||
object <- Utils.get_url(object),
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
|
||||
{:ok, activity, object} <- ActivityPub.join(object, actor, false, %{url: id}) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
Logger.debug(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Leave", "object" => object, "actor" => actor, "id" => _id} = data
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
{:ok, %Actor{} = actor} <- Actors.get_actor_by_url(actor),
|
||||
object <- Utils.get_url(object),
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
|
||||
{:ok, activity, object} <- ActivityPub.leave(object, actor, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{:only_organizer, true} ->
|
||||
Logger.warn(
|
||||
"Actor #{inspect(actor)} tried to leave event #{inspect(object)} but it was the only organizer so we didn't detach it"
|
||||
)
|
||||
|
||||
:error
|
||||
|
||||
_e ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# # TODO
|
||||
# # Accept
|
||||
# # Undo
|
||||
#
|
||||
# def handle_incoming(
|
||||
# %{
|
||||
# "type" => "Undo",
|
||||
# "object" => %{"type" => "Like", "object" => object_id},
|
||||
# "actor" => _actor,
|
||||
# "id" => id
|
||||
# } = data
|
||||
# ) do
|
||||
# with actor <- get_actor(data),
|
||||
# %Actor{} = actor <- ActivityPub.get_or_fetch_actor_by_url(actor),
|
||||
# {:ok, object} <- fetch_obj_helper(object_id) || fetch_obj_helper(object_id),
|
||||
# {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
|
||||
# {:ok, activity}
|
||||
# else
|
||||
# _e -> :error
|
||||
# end
|
||||
# end
|
||||
|
||||
def handle_incoming(_) do
|
||||
Logger.info("Handing something not supported")
|
||||
{:error, :not_supported}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handle incoming `Accept` activities wrapping a `Follow` activity
|
||||
"""
|
||||
def do_handle_incoming_accept_following(follow_object, %Actor{} = actor) do
|
||||
with {:follow, {:ok, %Follower{approved: false, target_actor: followed} = follow}} <-
|
||||
{:follow, get_follow(follow_object)},
|
||||
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
|
||||
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
|
||||
ActivityPub.accept(
|
||||
:follow,
|
||||
follow,
|
||||
false
|
||||
) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
{:follow, _} ->
|
||||
Logger.debug(
|
||||
"Tried to handle an Accept activity but it's not containing a Follow activity"
|
||||
)
|
||||
|
||||
nil
|
||||
|
||||
{:same_actor} ->
|
||||
{:error, "Actor who accepted the follow wasn't the target. Quite odd."}
|
||||
|
||||
{:ok, %Follower{approved: true} = _follow} ->
|
||||
{:error, "Follow already accepted"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handle incoming `Reject` activities wrapping a `Follow` activity
|
||||
"""
|
||||
def do_handle_incoming_reject_following(follow_object, %Actor{} = actor) do
|
||||
with {:follow, {:ok, %Follower{approved: false, target_actor: followed} = follow}} <-
|
||||
{:follow, get_follow(follow_object)},
|
||||
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
|
||||
{:ok, activity, _} <-
|
||||
ActivityPub.reject(:follow, follow) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
{:follow, _} ->
|
||||
Logger.debug(
|
||||
"Tried to handle a Reject activity but it's not containing a Follow activity"
|
||||
)
|
||||
|
||||
nil
|
||||
|
||||
{:same_actor} ->
|
||||
{:error, "Actor who rejected the follow wasn't the target. Quite odd."}
|
||||
|
||||
{:ok, %Follower{approved: true} = _follow} ->
|
||||
{:error, "Follow already accepted"}
|
||||
end
|
||||
end
|
||||
|
||||
# Handle incoming `Accept` activities wrapping a `Join` activity on an event
|
||||
defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
|
||||
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
|
||||
{:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
|
||||
{:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <-
|
||||
ActivityPub.accept(
|
||||
:join,
|
||||
participant,
|
||||
false
|
||||
),
|
||||
:ok <-
|
||||
Participation.send_emails_to_local_user(participant) do
|
||||
{:ok, activity, participant}
|
||||
else
|
||||
{:join_event, {:ok, %Participant{role: :participant}}} ->
|
||||
Logger.debug(
|
||||
"Tried to handle an Accept activity on a Join activity with a event object but the participant is already validated"
|
||||
)
|
||||
|
||||
nil
|
||||
|
||||
{:join_event, _err} ->
|
||||
Logger.debug(
|
||||
"Tried to handle an Accept activity but it's not containing a Join activity on a event"
|
||||
)
|
||||
|
||||
nil
|
||||
|
||||
{:same_actor} ->
|
||||
{:error, "Actor who accepted the join wasn't the event organizer. Quite odd."}
|
||||
|
||||
{:ok, %Participant{role: :participant} = _follow} ->
|
||||
{:error, "Participant"}
|
||||
end
|
||||
end
|
||||
|
||||
# Handle incoming `Reject` activities wrapping a `Join` activity on an event
|
||||
defp do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do
|
||||
with {:join_event, {:ok, %Participant{event: event, role: role} = participant}}
|
||||
when role != :rejected <-
|
||||
{:join_event, get_participant(join_object)},
|
||||
# TODO: The actor that accepts the Join activity may another one that the event organizer ?
|
||||
# Or maybe for groups it's the group that sends the Accept activity
|
||||
{:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
|
||||
{:ok, activity, participant} <-
|
||||
ActivityPub.reject(:join, participant, false),
|
||||
:ok <- Participation.send_emails_to_local_user(participant) do
|
||||
{:ok, activity, participant}
|
||||
else
|
||||
{:join_event, {:ok, %Participant{role: :rejected}}} ->
|
||||
Logger.warn(
|
||||
"Tried to handle an Reject activity on a Join activity with a event object but the participant is already rejected"
|
||||
)
|
||||
|
||||
nil
|
||||
|
||||
{:join_event, _err} ->
|
||||
Logger.debug(
|
||||
"Tried to handle an Reject activity but it's not containing a Join activity on a event"
|
||||
)
|
||||
|
||||
nil
|
||||
|
||||
{:same_actor} ->
|
||||
{:error, "Actor who rejected the join wasn't the event organizer. Quite odd."}
|
||||
|
||||
{:ok, %Participant{role: :participant} = _follow} ->
|
||||
{:error, "Participant"}
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Add do_handle_incoming_accept_join/1 on Groups
|
||||
|
||||
defp get_follow(follow_object) do
|
||||
with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object),
|
||||
{:not_found, %Follower{} = follow} <-
|
||||
{:not_found, Actors.get_follower_by_url(follow_object_id)} do
|
||||
{:ok, follow}
|
||||
else
|
||||
{:not_found, _err} ->
|
||||
{:error, "Follow URL not found"}
|
||||
|
||||
_ ->
|
||||
{:error, "ActivityPub ID not found in Accept Follow object"}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_participant(join_object) do
|
||||
with join_object_id when not is_nil(join_object_id) <- Utils.get_url(join_object),
|
||||
{:not_found, %Participant{} = participant} <-
|
||||
{:not_found, Events.get_participant_by_url(join_object_id)} do
|
||||
{:ok, participant}
|
||||
else
|
||||
{:not_found, _err} ->
|
||||
{:error, "Participant URL not found"}
|
||||
|
||||
_ ->
|
||||
{:error, "ActivityPub ID not found in Accept Join object"}
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_outgoing(%{"type" => _type} = data) do
|
||||
data =
|
||||
data
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
@spec fetch_obj_helper(map() | String.t()) :: Event.t() | Comment.t() | Actor.t() | any()
|
||||
def fetch_obj_helper(object) do
|
||||
Logger.debug("fetch_obj_helper")
|
||||
Logger.debug("Fetching object #{inspect(object)}")
|
||||
|
||||
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
|
||||
{:ok, object} ->
|
||||
{:ok, object}
|
||||
|
||||
err ->
|
||||
Logger.warn("Error while fetching #{inspect(object)}")
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_obj_helper_as_activity_streams(object) do
|
||||
Logger.debug("fetch_obj_helper_as_activity_streams")
|
||||
|
||||
with {:ok, object} <- fetch_obj_helper(object) do
|
||||
{:ok, Convertible.model_to_as(object)}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,450 +0,0 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/utils.ex
|
||||
|
||||
defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
@moduledoc """
|
||||
# Various ActivityPub related utils.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Service.ActivityPub.{Activity, Converter}
|
||||
alias Mobilizon.Service.Federator
|
||||
|
||||
require Logger
|
||||
|
||||
@actor_types ["Group", "Person", "Application"]
|
||||
|
||||
# Some implementations send the actor URI as the actor field, others send the entire actor object,
|
||||
# so figure out what the actor's URI is based on what we have.
|
||||
def get_url(%{"id" => id}), do: id
|
||||
def get_url(id) when is_bitstring(id), do: id
|
||||
def get_url(_), do: nil
|
||||
|
||||
def make_json_ld_header do
|
||||
%{
|
||||
"@context" => [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://litepub.social/litepub/context.jsonld",
|
||||
%{
|
||||
"sc" => "http://schema.org#",
|
||||
"ical" => "http://www.w3.org/2002/12/cal/ical#",
|
||||
"pt" => "https://joinpeertube.org/ns#",
|
||||
"Hashtag" => "as:Hashtag",
|
||||
"category" => "sc:category",
|
||||
"uuid" => "sc:identifier",
|
||||
"maximumAttendeeCapacity" => "sc:maximumAttendeeCapacity",
|
||||
"location" => %{
|
||||
"@id" => "sc:location",
|
||||
"@type" => "sc:Place"
|
||||
},
|
||||
"PostalAddress" => "sc:PostalAddress",
|
||||
"address" => %{
|
||||
"@id" => "sc:address",
|
||||
"@type" => "sc:PostalAddress"
|
||||
},
|
||||
"addressCountry" => "sc:addressCountry",
|
||||
"addressRegion" => "sc:addressRegion",
|
||||
"postalCode" => "sc:postalCode",
|
||||
"addressLocality" => "sc:addressLocality",
|
||||
"streetAddress" => "sc:streetAddress",
|
||||
"mz" => "https://joinmobilizon.org/ns#",
|
||||
"repliesModerationOptionType" => %{
|
||||
"@id" => "mz:repliesModerationOptionType",
|
||||
"@type" => "rdfs:Class"
|
||||
},
|
||||
"repliesModerationOption" => %{
|
||||
"@id" => "mz:repliesModerationOption",
|
||||
"@type" => "mz:repliesModerationOptionType"
|
||||
},
|
||||
"commentsEnabled" => %{
|
||||
"@type" => "sc:Boolean",
|
||||
"@id" => "pt:commentsEnabled"
|
||||
},
|
||||
"joinModeType" => %{
|
||||
"@id" => "mz:joinModeType",
|
||||
"@type" => "rdfs:Class"
|
||||
},
|
||||
"joinMode" => %{
|
||||
"@id" => "mz:joinMode",
|
||||
"@type" => "mz:joinModeType"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def make_date do
|
||||
DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enqueues an activity for federation if it's local
|
||||
"""
|
||||
def maybe_federate(%Activity{local: true} = activity) do
|
||||
Logger.debug("Maybe federate an activity")
|
||||
|
||||
if Mobilizon.Config.get!([:instance, :federating]) do
|
||||
priority =
|
||||
case activity.data["type"] do
|
||||
"Delete" -> 10
|
||||
"Create" -> 1
|
||||
_ -> 5
|
||||
end
|
||||
|
||||
Federator.enqueue(:publish, activity, priority)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def maybe_federate(_), do: :ok
|
||||
|
||||
def remote_actors(%{data: %{"to" => to} = data}) do
|
||||
to = to ++ (data["cc"] || [])
|
||||
|
||||
to
|
||||
|> Enum.map(fn url -> Actors.get_actor_by_url(url) end)
|
||||
|> Enum.map(fn {status, actor} ->
|
||||
case status do
|
||||
:ok ->
|
||||
actor
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.map(& &1)
|
||||
|> Enum.filter(fn actor -> actor && !is_nil(actor.domain) end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an id and a published data if they aren't there,
|
||||
also adds it to an included object
|
||||
"""
|
||||
def lazy_put_activity_defaults(map) do
|
||||
if is_map(map["object"]) do
|
||||
object = lazy_put_object_defaults(map["object"])
|
||||
%{map | "object" => object}
|
||||
else
|
||||
map
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an id and published date if they aren't there.
|
||||
"""
|
||||
def lazy_put_object_defaults(map) do
|
||||
Map.put_new_lazy(map, "published", &make_date/0)
|
||||
end
|
||||
|
||||
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 """
|
||||
Checks that an incoming AP object's actor matches the domain it came from.
|
||||
"""
|
||||
def origin_check?(id, %{"actor" => actor} = params) when not is_nil(actor) do
|
||||
id_uri = URI.parse(id)
|
||||
actor_uri = URI.parse(get_actor(params))
|
||||
|
||||
compare_uris?(actor_uri, id_uri)
|
||||
end
|
||||
|
||||
def origin_check?(_id, %{"actor" => nil}), do: false
|
||||
|
||||
def origin_check?(id, %{"attributedTo" => actor} = params),
|
||||
do: origin_check?(id, Map.put(params, "actor", actor))
|
||||
|
||||
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 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.
|
||||
"""
|
||||
def make_picture_data(%Plug.Upload{} = picture, opts) do
|
||||
case MobilizonWeb.Upload.store(picture, opts) do
|
||||
{:ok, picture} ->
|
||||
picture
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert a picture model into an AS Link representation.
|
||||
"""
|
||||
def make_picture_data(%Picture{} = picture) do
|
||||
Converter.Picture.model_to_as(picture)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Save picture data from raw data and return AS Link data.
|
||||
"""
|
||||
def make_picture_data(picture) when is_map(picture) do
|
||||
with {:ok, %{"url" => [%{"href" => url, "mediaType" => content_type}], "size" => size}} <-
|
||||
MobilizonWeb.Upload.store(picture.file),
|
||||
{:picture_exists, nil} <- {:picture_exists, Mobilizon.Media.get_picture_by_url(url)},
|
||||
{:ok, %Picture{file: _file} = picture} <-
|
||||
Mobilizon.Media.create_picture(%{
|
||||
"file" => %{
|
||||
"url" => url,
|
||||
"name" => picture.name,
|
||||
"content_type" => content_type,
|
||||
"size" => size
|
||||
},
|
||||
"actor_id" => picture.actor_id
|
||||
}) do
|
||||
Converter.Picture.model_to_as(picture)
|
||||
else
|
||||
{:picture_exists, %Picture{file: _file} = picture} ->
|
||||
Converter.Picture.model_to_as(picture)
|
||||
|
||||
err ->
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def make_picture_data(nil), do: nil
|
||||
|
||||
@doc """
|
||||
Make announce activity data for the given actor and object
|
||||
"""
|
||||
def make_announce_data(actor, object, activity_id, public \\ true)
|
||||
|
||||
def make_announce_data(
|
||||
%Actor{url: actor_url, followers_url: actor_followers_url} = _actor,
|
||||
%{"id" => url, "type" => type} = _object,
|
||||
activity_id,
|
||||
public
|
||||
)
|
||||
when type in @actor_types do
|
||||
do_make_announce_data(actor_url, actor_followers_url, url, url, activity_id, public)
|
||||
end
|
||||
|
||||
def make_announce_data(
|
||||
%Actor{url: actor_url, followers_url: actor_followers_url} = _actor,
|
||||
%{"id" => url, "type" => type, "actor" => object_actor_url} = _object,
|
||||
activity_id,
|
||||
public
|
||||
)
|
||||
when type in ["Note", "Event"] do
|
||||
do_make_announce_data(
|
||||
actor_url,
|
||||
actor_followers_url,
|
||||
object_actor_url,
|
||||
url,
|
||||
activity_id,
|
||||
public
|
||||
)
|
||||
end
|
||||
|
||||
defp do_make_announce_data(
|
||||
actor_url,
|
||||
actor_followers_url,
|
||||
object_actor_url,
|
||||
object_url,
|
||||
activity_id,
|
||||
public
|
||||
) do
|
||||
{to, cc} =
|
||||
if public do
|
||||
{[actor_followers_url, object_actor_url],
|
||||
["https://www.w3.org/ns/activitystreams#Public"]}
|
||||
else
|
||||
{[actor_followers_url], []}
|
||||
end
|
||||
|
||||
data = %{
|
||||
"type" => "Announce",
|
||||
"actor" => actor_url,
|
||||
"object" => object_url,
|
||||
"to" => to,
|
||||
"cc" => cc
|
||||
}
|
||||
|
||||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make unannounce activity data for the given actor and object
|
||||
"""
|
||||
def make_unannounce_data(
|
||||
%Actor{url: url} = actor,
|
||||
activity,
|
||||
activity_id
|
||||
) do
|
||||
data = %{
|
||||
"type" => "Undo",
|
||||
"actor" => url,
|
||||
"object" => activity,
|
||||
"to" => [actor.followers_url, actor.url],
|
||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||||
end
|
||||
|
||||
#### Unfollow-related helpers
|
||||
|
||||
@spec make_unfollow_data(Actor.t(), Actor.t(), map(), String.t()) :: map()
|
||||
def make_unfollow_data(
|
||||
%Actor{url: follower_url},
|
||||
%Actor{url: followed_url},
|
||||
follow_activity,
|
||||
activity_id
|
||||
) do
|
||||
data = %{
|
||||
"type" => "Undo",
|
||||
"actor" => follower_url,
|
||||
"to" => [followed_url],
|
||||
"object" => follow_activity.data
|
||||
}
|
||||
|
||||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||||
end
|
||||
|
||||
#### Create-related helpers
|
||||
|
||||
@doc """
|
||||
Make create activity data
|
||||
"""
|
||||
@spec make_create_data(map(), map()) :: map()
|
||||
def make_create_data(object, additional \\ %{}) do
|
||||
Logger.debug("Making create data")
|
||||
Logger.debug(inspect(object))
|
||||
Logger.debug(inspect(additional))
|
||||
|
||||
%{
|
||||
"type" => "Create",
|
||||
"to" => object["to"],
|
||||
"cc" => object["cc"],
|
||||
"actor" => object["actor"],
|
||||
"object" => object,
|
||||
"published" => make_date(),
|
||||
"id" => object["id"] <> "/activity"
|
||||
}
|
||||
|> Map.merge(additional)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make update activity data
|
||||
"""
|
||||
@spec make_update_data(map(), map()) :: map()
|
||||
def make_update_data(object, additional \\ %{}) do
|
||||
Logger.debug("Making update data")
|
||||
Logger.debug(inspect(object))
|
||||
Logger.debug(inspect(additional))
|
||||
|
||||
%{
|
||||
"type" => "Update",
|
||||
"to" => object["to"],
|
||||
"cc" => object["cc"],
|
||||
"actor" => object["actor"],
|
||||
"object" => object,
|
||||
"id" => object["id"] <> "/activity"
|
||||
}
|
||||
|> Map.merge(additional)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make accept join activity data
|
||||
"""
|
||||
@spec make_accept_join_data(map(), map()) :: map()
|
||||
def make_accept_join_data(object, additional \\ %{}) do
|
||||
%{
|
||||
"type" => "Accept",
|
||||
"to" => object["to"],
|
||||
"cc" => object["cc"],
|
||||
"object" => object,
|
||||
"id" => object["id"] <> "/activity"
|
||||
}
|
||||
|> Map.merge(additional)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts PEM encoded keys to a public key representation
|
||||
"""
|
||||
def pem_to_public_key(pem) do
|
||||
[key_code] = :public_key.pem_decode(pem)
|
||||
key = :public_key.pem_entry_decode(key_code)
|
||||
|
||||
case key do
|
||||
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} ->
|
||||
{:RSAPublicKey, modulus, exponent}
|
||||
|
||||
{:RSAPublicKey, modulus, exponent} ->
|
||||
{:RSAPublicKey, modulus, exponent}
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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 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 maybe_date_fetch(headers, date) do
|
||||
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do
|
||||
headers ++ [{:Date, date}]
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/visibility.ex
|
||||
|
||||
defmodule Mobilizon.Service.ActivityPub.Visibility do
|
||||
@moduledoc """
|
||||
Utility functions related to content visibility
|
||||
"""
|
||||
|
||||
alias Mobilizon.Events.Comment
|
||||
alias Mobilizon.Service.ActivityPub.Activity
|
||||
|
||||
@public "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
@spec is_public?(Activity.t() | map()) :: boolean()
|
||||
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 (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
|
||||
@@ -1,141 +0,0 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/federator/federator.ex
|
||||
|
||||
defmodule Mobilizon.Service.Federator do
|
||||
@moduledoc """
|
||||
Handle federated activities
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.ActivityPub.{Activity, Transmogrifier}
|
||||
|
||||
require Logger
|
||||
|
||||
@max_jobs 20
|
||||
|
||||
def init(args) do
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
def start_link(_) do
|
||||
spawn(fn ->
|
||||
# 1 minute
|
||||
Process.sleep(1000 * 60)
|
||||
end)
|
||||
|
||||
GenServer.start_link(
|
||||
__MODULE__,
|
||||
%{
|
||||
in: {:sets.new(), []},
|
||||
out: {:sets.new(), []}
|
||||
},
|
||||
name: __MODULE__
|
||||
)
|
||||
end
|
||||
|
||||
def handle(:publish, activity) do
|
||||
Logger.debug(inspect(activity))
|
||||
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
|
||||
|
||||
with actor when not is_nil(actor) <- Actors.get_actor_by_url!(activity.data["actor"]) do
|
||||
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
|
||||
ActivityPub.publish(actor, activity)
|
||||
end
|
||||
end
|
||||
|
||||
def handle(:incoming_ap_doc, params) do
|
||||
Logger.info("Handling incoming AP activity")
|
||||
Logger.debug(inspect(params))
|
||||
|
||||
case Transmogrifier.handle_incoming(params) do
|
||||
{:ok, activity, _data} ->
|
||||
{:ok, activity}
|
||||
|
||||
%Activity{} ->
|
||||
Logger.info("Already had #{params["id"]}")
|
||||
|
||||
e ->
|
||||
# Just drop those for now
|
||||
Logger.error("Unhandled activity")
|
||||
Logger.debug(inspect(e))
|
||||
Logger.debug(Jason.encode!(params))
|
||||
end
|
||||
end
|
||||
|
||||
def handle(:publish_single_ap, params) do
|
||||
ActivityPub.publish_one(params)
|
||||
end
|
||||
|
||||
def handle(type, _) do
|
||||
Logger.debug(fn -> "Unknown task: #{type}" end)
|
||||
{:error, "Don't know what to do with this"}
|
||||
end
|
||||
|
||||
def enqueue(type, payload, priority \\ 1) do
|
||||
Logger.debug("enqueue something with type #{inspect(type)}")
|
||||
|
||||
if Mix.env() == :test do
|
||||
handle(type, payload)
|
||||
else
|
||||
GenServer.cast(__MODULE__, {:enqueue, type, payload, priority})
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_start_job(running_jobs, queue) do
|
||||
if :sets.size(running_jobs) < @max_jobs && queue != [] do
|
||||
{{type, payload}, queue} = queue_pop(queue)
|
||||
{:ok, pid} = Task.start(fn -> handle(type, payload) end)
|
||||
mref = Process.monitor(pid)
|
||||
{:sets.add_element(mref, running_jobs), queue}
|
||||
else
|
||||
{running_jobs, queue}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_cast({:enqueue, type, payload, _priority}, state)
|
||||
when type in [:incoming_doc, :incoming_ap_doc] do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
i_queue = enqueue_sorted(i_queue, {type, payload}, 1)
|
||||
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
|
||||
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
|
||||
end
|
||||
|
||||
def handle_cast({:enqueue, type, payload, _priority}, state) do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
o_queue = enqueue_sorted(o_queue, {type, payload}, 1)
|
||||
{o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
|
||||
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
|
||||
end
|
||||
|
||||
def handle_cast(m, state) do
|
||||
Logger.debug(fn ->
|
||||
"Unknown: #{inspect(m)}, #{inspect(state)}"
|
||||
end)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
i_running_jobs = :sets.del_element(ref, i_running_jobs)
|
||||
o_running_jobs = :sets.del_element(ref, o_running_jobs)
|
||||
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
|
||||
{o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
|
||||
|
||||
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
|
||||
end
|
||||
|
||||
def enqueue_sorted(queue, element, priority) do
|
||||
[%{item: element, priority: priority} | queue]
|
||||
|> Enum.sort_by(fn %{priority: priority} -> priority end)
|
||||
end
|
||||
|
||||
def queue_pop([%{item: element} | queue]) do
|
||||
{element, queue}
|
||||
end
|
||||
end
|
||||
@@ -3,21 +3,15 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/html.ex
|
||||
|
||||
defmodule Mobilizon.Service.HTML do
|
||||
defmodule Mobilizon.Service.Formatter.DefaultScrubbler do
|
||||
@moduledoc """
|
||||
Service to filter tags out of HTML content
|
||||
Custom strategy to filter HTML content.
|
||||
"""
|
||||
alias HtmlSanitizeEx.Scrubber
|
||||
alias Mobilizon.Service.HTML.Scrubber.Default
|
||||
|
||||
def filter_tags(html), do: Scrubber.scrub(html, Default)
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Service.HTML.Scrubber.Default do
|
||||
@moduledoc "Custom strategy to filter HTML content"
|
||||
alias HtmlSanitizeEx.Scrubber.Meta
|
||||
|
||||
require HtmlSanitizeEx.Scrubber.Meta
|
||||
alias HtmlSanitizeEx.Scrubber.Meta
|
||||
|
||||
# credo:disable-for-previous-line
|
||||
# No idea how to fix this one…
|
||||
|
||||
@@ -10,18 +10,18 @@ defmodule Mobilizon.Service.Formatter do
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.HTML
|
||||
alias Mobilizon.Service.Formatter.HTML
|
||||
|
||||
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
|
||||
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
|
||||
|
||||
@auto_linker_config hashtag: true,
|
||||
hashtag_handler: &Mobilizon.Service.Formatter.hashtag_handler/4,
|
||||
hashtag_handler: &__MODULE__.hashtag_handler/4,
|
||||
mention: true,
|
||||
mention_handler: &Mobilizon.Service.Formatter.mention_handler/4
|
||||
mention_handler: &__MODULE__.mention_handler/4
|
||||
|
||||
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
|
||||
case Mobilizon.Actors.get_actor_by_name(nickname) do
|
||||
case Actors.get_actor_by_name(nickname) do
|
||||
%Actor{} ->
|
||||
# escape markdown characters with `\\`
|
||||
# (we don't want something like @user__name to be parsed by markdown)
|
||||
|
||||
17
lib/service/formatter/html.ex
Normal file
17
lib/service/formatter/html.ex
Normal file
@@ -0,0 +1,17 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/html.ex
|
||||
|
||||
defmodule Mobilizon.Service.Formatter.HTML do
|
||||
@moduledoc """
|
||||
Service to filter tags out of HTML content.
|
||||
"""
|
||||
|
||||
alias HtmlSanitizeEx.Scrubber
|
||||
|
||||
alias Mobilizon.Service.Formatter.DefaultScrubbler
|
||||
|
||||
def filter_tags(html), do: Scrubber.scrub(html, DefaultScrubbler)
|
||||
end
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/signature.ex
|
||||
|
||||
defmodule Mobilizon.Service.HTTPSignatures.Signature do
|
||||
@moduledoc """
|
||||
Adapter for the `HTTPSignatures` lib that handles signing and providing public keys to verify HTTPSignatures
|
||||
"""
|
||||
|
||||
@behaviour HTTPSignatures.Adapter
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
|
||||
require Logger
|
||||
|
||||
@spec key_id_to_actor_url(String.t()) :: String.t()
|
||||
def key_id_to_actor_url(key_id) do
|
||||
%{path: path} =
|
||||
uri =
|
||||
key_id
|
||||
|> URI.parse()
|
||||
|> Map.put(:fragment, nil)
|
||||
|
||||
uri =
|
||||
if is_nil(path) do
|
||||
uri
|
||||
else
|
||||
Map.put(uri, :path, String.trim_trailing(path, "/publickey"))
|
||||
end
|
||||
|
||||
URI.to_string(uri)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert internal PEM encoded keys to public key format.
|
||||
"""
|
||||
@spec prepare_public_key(String.t()) :: {:ok, tuple} | {:error, :pem_decode_error}
|
||||
def prepare_public_key(public_key_code) do
|
||||
case :public_key.pem_decode(public_key_code) do
|
||||
[public_key_entry] ->
|
||||
{:ok, :public_key.pem_entry_decode(public_key_entry)}
|
||||
|
||||
_ ->
|
||||
{:error, :pem_decode_error}
|
||||
end
|
||||
end
|
||||
|
||||
# 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}
|
||||
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}
|
||||
else
|
||||
{:error, :pem_decode_error} ->
|
||||
Logger.error("Error while decoding PEM")
|
||||
|
||||
{:error, :pem_decode_error}
|
||||
|
||||
_ ->
|
||||
Logger.error("Unable to fetch actor, so no keys for you")
|
||||
|
||||
{:error, :actor_fetch_error}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
actor_id <- key_id_to_actor_url(kid),
|
||||
:ok <- Logger.debug("Fetching public key for #{actor_id}"),
|
||||
{:ok, public_key} <- get_public_key_for_url(actor_id) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def refetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
actor_id <- key_id_to_actor_url(kid),
|
||||
:ok <- Logger.debug("Refetching public key for #{actor_id}"),
|
||||
{:ok, _actor} <- ActivityPub.make_actor_from_url(actor_id),
|
||||
{:ok, public_key} <- get_public_key_for_url(actor_id) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def sign(%Actor{keys: keys} = actor, headers) do
|
||||
Logger.debug("Signing on behalf of #{actor.url}")
|
||||
Logger.debug("headers")
|
||||
Logger.debug(inspect(headers))
|
||||
|
||||
with {:ok, key} <- prepare_public_key(keys) do
|
||||
HTTPSignatures.sign(key, actor.url <> "#main-key", headers)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_date_header, do: generate_date_header(NaiveDateTime.utc_now())
|
||||
|
||||
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}"
|
||||
|
||||
def build_digest(body) do
|
||||
"SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64())
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
defprotocol Mobilizon.Service.Metadata do
|
||||
@doc """
|
||||
Build tags
|
||||
"""
|
||||
def build_tags(entity)
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Service.MetadataUtils do
|
||||
@moduledoc """
|
||||
Tools to convert tags to string
|
||||
"""
|
||||
alias Phoenix.HTML
|
||||
|
||||
def stringify_tags(tags) do
|
||||
Enum.reduce(tags, "", &stringify_tag/2)
|
||||
end
|
||||
|
||||
defp stringify_tag(tag, acc) when is_tuple(tag), do: acc <> HTML.safe_to_string(tag)
|
||||
defp stringify_tag(tag, acc) when is_binary(tag), do: acc <> tag
|
||||
end
|
||||
7
lib/service/metadata/metadata.ex
Normal file
7
lib/service/metadata/metadata.ex
Normal file
@@ -0,0 +1,7 @@
|
||||
defprotocol Mobilizon.Service.Metadata do
|
||||
@doc """
|
||||
Build tags
|
||||
"""
|
||||
|
||||
def build_tags(entity)
|
||||
end
|
||||
12
lib/service/metadata/utils.ex
Normal file
12
lib/service/metadata/utils.ex
Normal file
@@ -0,0 +1,12 @@
|
||||
defmodule Mobilizon.Service.Metadata.Utils do
|
||||
@moduledoc """
|
||||
Tools to convert tags to string.
|
||||
"""
|
||||
|
||||
alias Phoenix.HTML
|
||||
|
||||
def stringify_tags(tags), do: Enum.reduce(tags, "", &stringify_tag/2)
|
||||
|
||||
defp stringify_tag(tag, acc) when is_tuple(tag), do: acc <> HTML.safe_to_string(tag)
|
||||
defp stringify_tag(tag, acc) when is_binary(tag), do: acc <> tag
|
||||
end
|
||||
@@ -1,127 +0,0 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/web_finger/web_finger.ex
|
||||
|
||||
defmodule Mobilizon.Service.WebFinger do
|
||||
@moduledoc """
|
||||
# WebFinger
|
||||
|
||||
Performs the WebFinger requests and responses (JSON only)
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.XmlBuilder
|
||||
require Jason
|
||||
require Logger
|
||||
|
||||
def host_meta do
|
||||
base_url = MobilizonWeb.Endpoint.url()
|
||||
|
||||
{
|
||||
:XRD,
|
||||
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
|
||||
{
|
||||
:Link,
|
||||
%{
|
||||
rel: "lrdd",
|
||||
type: "application/xrd+xml",
|
||||
template: "#{base_url}/.well-known/webfinger?resource={uri}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|> XmlBuilder.to_doc()
|
||||
end
|
||||
|
||||
def webfinger(resource, "JSON") do
|
||||
host = MobilizonWeb.Endpoint.host()
|
||||
regex = ~r/(acct:)?(?<name>\w+)@#{host}/
|
||||
|
||||
with %{"name" => name} <- Regex.named_captures(regex, resource),
|
||||
%Actor{} = actor <- Actors.get_local_actor_by_name(name) do
|
||||
{:ok, represent_actor(actor, "JSON")}
|
||||
else
|
||||
_e ->
|
||||
case Actors.get_actor_by_url(resource) do
|
||||
{:ok, %Actor{} = actor} when not is_nil(actor) ->
|
||||
{:ok, represent_actor(actor, "JSON")}
|
||||
|
||||
_e ->
|
||||
{:error, "Couldn't find actor"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec represent_actor(Actor.t()) :: struct()
|
||||
def represent_actor(actor), do: represent_actor(actor, "JSON")
|
||||
|
||||
@spec represent_actor(Actor.t(), String.t()) :: struct()
|
||||
def represent_actor(actor, "JSON") do
|
||||
%{
|
||||
"subject" => "acct:#{actor.preferred_username}@#{MobilizonWeb.Endpoint.host()}",
|
||||
"aliases" => [actor.url],
|
||||
"links" => [
|
||||
%{"rel" => "self", "type" => "application/activity+json", "href" => actor.url},
|
||||
%{
|
||||
"rel" => "https://webfinger.net/rel/profile-page/",
|
||||
"type" => "text/html",
|
||||
"href" => actor.url
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp webfinger_from_json(doc) do
|
||||
data =
|
||||
Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
|
||||
case {link["type"], link["rel"]} do
|
||||
{"application/activity+json", "self"} ->
|
||||
Map.put(data, "url", link["href"])
|
||||
|
||||
_ ->
|
||||
Logger.debug(fn ->
|
||||
"Unhandled type: #{inspect(link["type"])}"
|
||||
end)
|
||||
|
||||
data
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
def finger(actor) do
|
||||
actor = String.trim_leading(actor, "@")
|
||||
|
||||
domain =
|
||||
case String.split(actor, "@") do
|
||||
[_name, domain] ->
|
||||
domain
|
||||
|
||||
_e ->
|
||||
URI.parse(actor).host
|
||||
end
|
||||
|
||||
address = "http://#{domain}/.well-known/webfinger?resource=acct:#{actor}"
|
||||
|
||||
Logger.debug(inspect(address))
|
||||
|
||||
with false <- is_nil(domain),
|
||||
{:ok, %HTTPoison.Response{} = response} <-
|
||||
HTTPoison.get(
|
||||
address,
|
||||
[Accept: "application/json, application/activity+json, application/jrd+json"],
|
||||
follow_redirect: true
|
||||
),
|
||||
%{status_code: status_code, body: body} when status_code in 200..299 <- response,
|
||||
{:ok, doc} <- Jason.decode(body) do
|
||||
webfinger_from_json(doc)
|
||||
else
|
||||
e ->
|
||||
Logger.debug(fn -> "Couldn't finger #{actor}" end)
|
||||
Logger.debug(fn -> inspect(e) end)
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,55 +0,0 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/xml_builder.ex
|
||||
|
||||
defmodule Mobilizon.Service.XmlBuilder do
|
||||
@moduledoc """
|
||||
XML Builder.
|
||||
|
||||
Needed to build XRD for webfinger host_meta
|
||||
"""
|
||||
|
||||
def to_xml({tag, attributes, content}) do
|
||||
open_tag = make_open_tag(tag, attributes)
|
||||
|
||||
content_xml = to_xml(content)
|
||||
|
||||
"<#{open_tag}>#{content_xml}</#{tag}>"
|
||||
end
|
||||
|
||||
def to_xml({tag, %{} = attributes}) do
|
||||
open_tag = make_open_tag(tag, attributes)
|
||||
|
||||
"<#{open_tag} />"
|
||||
end
|
||||
|
||||
def to_xml({tag, content}), do: to_xml({tag, %{}, content})
|
||||
|
||||
def to_xml(content) when is_binary(content) do
|
||||
to_string(content)
|
||||
end
|
||||
|
||||
def to_xml(content) when is_list(content) do
|
||||
for element <- content do
|
||||
to_xml(element)
|
||||
end
|
||||
|> Enum.join()
|
||||
end
|
||||
|
||||
def to_xml(%NaiveDateTime{} = time) do
|
||||
NaiveDateTime.to_iso8601(time)
|
||||
end
|
||||
|
||||
def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(content)
|
||||
|
||||
defp make_open_tag(tag, attributes) do
|
||||
attributes_string =
|
||||
for {attribute, value} <- attributes do
|
||||
"#{attribute}=\"#{value}\""
|
||||
end
|
||||
|> Enum.join(" ")
|
||||
|
||||
[tag, attributes_string] |> Enum.join(" ") |> String.trim()
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user