Various refactoring and typespec improvements
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -75,7 +75,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
"""
|
||||
# TODO: Make database calls parallel
|
||||
@spec fetch_object_from_url(String.t(), Keyword.t()) ::
|
||||
{:ok, struct()} | {:error, any()}
|
||||
{:ok, struct()} | {:ok, atom(), struct()} | {:error, any()}
|
||||
def fetch_object_from_url(url, options \\ []) do
|
||||
Logger.info("Fetching object from url #{url}")
|
||||
|
||||
@@ -111,7 +111,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
|
||||
@spec handle_existing_entity(String.t(), struct(), Keyword.t()) ::
|
||||
{:ok, struct()}
|
||||
| {:ok, struct()}
|
||||
| {:ok, atom(), struct()}
|
||||
| {:error, String.t(), struct()}
|
||||
| {:error, String.t()}
|
||||
defp handle_existing_entity(url, entity, options) do
|
||||
@@ -126,13 +126,13 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
{:ok, entity} = Preloader.maybe_preload(entity)
|
||||
{:error, status, entity}
|
||||
|
||||
err ->
|
||||
err
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec refresh_entity(String.t(), struct(), Keyword.t()) ::
|
||||
{:ok, struct()} | {:error, String.t(), struct()} | {:error, String.t()}
|
||||
{:ok, struct()} | {:error, atom(), struct()} | {:error, String.t()}
|
||||
defp refresh_entity(url, entity, options) do
|
||||
force_fetch = Keyword.get(options, :force, false)
|
||||
|
||||
@@ -205,21 +205,22 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec update(Entity.entities(), map(), boolean, map()) ::
|
||||
{:ok, Activity.t(), Entity.entities()} | any()
|
||||
{:ok, Activity.t(), Entity.entities()} | {:error, any()}
|
||||
def update(old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("updating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:ok, entity, update_data} <- Managable.update(old_entity, args, additional),
|
||||
{:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
case Managable.update(old_entity, args, additional) do
|
||||
{:ok, entity, update_data} ->
|
||||
{:ok, activity} = create_activity(update_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, entity}
|
||||
|
||||
{:error, err} ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -274,7 +275,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
|
||||
@spec announce(Actor.t(), ActivityStream.t(), String.t() | nil, boolean, boolean) ::
|
||||
{:ok, Activity.t(), ActivityStream.t()}
|
||||
{:ok, Activity.t(), ActivityStream.t()} | {:error, any()}
|
||||
def announce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
@@ -318,7 +319,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
Make an actor follow another
|
||||
"""
|
||||
@spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean, map) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, String.t()}
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom | Ecto.Changeset.t() | String.t()}
|
||||
def follow(
|
||||
%Actor{} = follower,
|
||||
%Actor{} = followed,
|
||||
@@ -326,23 +327,23 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
with {:different_actors, true} <- {:different_actors, followed.id != follower.id},
|
||||
{:ok, activity_data, %Follower{} = follower} <-
|
||||
Types.Actors.follow(
|
||||
if followed.id != follower.id do
|
||||
case Types.Actors.follow(
|
||||
follower,
|
||||
followed,
|
||||
local,
|
||||
Map.merge(additional, %{"activity_id" => activity_id})
|
||||
),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, follower}
|
||||
else
|
||||
{:error, err, msg} when err in [:already_following, :suspended, :no_person] ->
|
||||
{:error, msg}
|
||||
) do
|
||||
{:ok, activity_data, %Follower{} = follower} ->
|
||||
{:ok, activity} = create_activity(activity_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, follower}
|
||||
|
||||
{:different_actors, _} ->
|
||||
{:error, "Can't follow yourself"}
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
else
|
||||
{:error, "Can't follow yourself"}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -350,7 +351,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
Make an actor unfollow another
|
||||
"""
|
||||
@spec unfollow(Actor.t(), Actor.t(), String.t() | nil, boolean()) ::
|
||||
{:ok, Activity.t(), Follower.t()}
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, String.t()}
|
||||
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
|
||||
@@ -385,18 +386,19 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
|
||||
@spec join(Event.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Participant.t()} | {:maximum_attendee_capacity, any}
|
||||
{:ok, Activity.t(), Participant.t()} | {:error, :maximum_attendee_capacity}
|
||||
@spec join(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
|
||||
def join(entity_to_join, actor_joining, local \\ true, additional \\ %{})
|
||||
|
||||
def join(%Event{} = event, %Actor{} = actor, local, additional) do
|
||||
with {:ok, activity_data, participant} <- Types.Events.join(event, actor, local, additional),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, participant}
|
||||
else
|
||||
{:maximum_attendee_capacity, err} ->
|
||||
{:maximum_attendee_capacity, err}
|
||||
case Types.Events.join(event, actor, local, additional) do
|
||||
{:ok, activity_data, participant} ->
|
||||
{:ok, activity} = create_activity(activity_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, participant}
|
||||
|
||||
{:error, :maximum_attendee_capacity_reached} ->
|
||||
{:error, :maximum_attendee_capacity_reached}
|
||||
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
@@ -415,7 +417,9 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
@spec leave(Event.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Participant.t()}
|
||||
@spec leave(Event.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Participant.t()}
|
||||
| {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()}
|
||||
@spec leave(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
|
||||
def leave(object, actor, local \\ true, additional \\ %{})
|
||||
|
||||
@@ -428,28 +432,37 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
local,
|
||||
additional
|
||||
) do
|
||||
with {:only_organizer, false} <-
|
||||
{:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)},
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.get_participant(
|
||||
if Participant.is_not_only_organizer(event_id, actor_id) do
|
||||
{:error, :is_only_organizer}
|
||||
else
|
||||
case Mobilizon.Events.get_participant(
|
||||
event_id,
|
||||
actor_id,
|
||||
Map.get(additional, :metadata, %{})
|
||||
),
|
||||
{: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" => "#{Endpoint.url()}/leave/event/#{participant.id}"
|
||||
},
|
||||
audience <-
|
||||
Audience.get_audience(participant),
|
||||
{:ok, activity} <- create_activity(Map.merge(leave_data, audience), local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, participant}
|
||||
) do
|
||||
{:ok, %Participant{} = participant} ->
|
||||
case Events.delete_participant(participant) do
|
||||
{:ok, %{participant: %Participant{} = participant}} ->
|
||||
leave_data = %{
|
||||
"type" => "Leave",
|
||||
# If it's an exclusion it should be something else
|
||||
"actor" => actor_url,
|
||||
"object" => event_url,
|
||||
"id" => "#{Endpoint.url()}/leave/event/#{participant.id}"
|
||||
}
|
||||
|
||||
audience = Audience.get_audience(participant)
|
||||
{:ok, activity} = create_activity(Map.merge(leave_data, audience), local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, participant}
|
||||
|
||||
{:error, _type, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, :participant_not_found} ->
|
||||
{:error, :participant_not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -24,22 +24,17 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
|
||||
def get_or_fetch_actor_by_url(nil, _preload), do: {:error, :url_nil}
|
||||
|
||||
def get_or_fetch_actor_by_url("https://www.w3.org/ns/activitystreams#Public", _preload) do
|
||||
case Relay.get_actor() do
|
||||
%Actor{url: url} ->
|
||||
get_or_fetch_actor_by_url(url)
|
||||
|
||||
{:error, %Ecto.Changeset{}} ->
|
||||
{:error, :no_internal_relay_actor}
|
||||
end
|
||||
%Actor{url: url} = Relay.get_actor()
|
||||
get_or_fetch_actor_by_url(url)
|
||||
end
|
||||
|
||||
def get_or_fetch_actor_by_url(url, preload) do
|
||||
case Actors.get_actor_by_url(url, preload) do
|
||||
{:ok, %Actor{} = cached_actor} ->
|
||||
unless Actors.needs_update?(cached_actor) do
|
||||
{:ok, cached_actor}
|
||||
else
|
||||
if Actors.needs_update?(cached_actor) do
|
||||
__MODULE__.make_actor_from_url(url, preload)
|
||||
else
|
||||
{:ok, cached_actor}
|
||||
end
|
||||
|
||||
{:error, :actor_not_found} ->
|
||||
|
||||
@@ -12,8 +12,8 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityPub.Transmogrifier
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -58,9 +58,6 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
{:ok, activity, _data} ->
|
||||
{:ok, activity}
|
||||
|
||||
%Activity{} ->
|
||||
Logger.info("Already had #{params["id"]}")
|
||||
|
||||
e ->
|
||||
# Just drop those for now
|
||||
Logger.debug("Unhandled activity")
|
||||
|
||||
@@ -20,7 +20,6 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
@spec fetch(String.t(), Keyword.t()) ::
|
||||
{:ok, map()}
|
||||
| {:ok, Tesla.Env.t()}
|
||||
| {:error, String.t()}
|
||||
| {:error, any()}
|
||||
| {:error, :invalid_url}
|
||||
def fetch(url, options \\ []) do
|
||||
@@ -109,7 +108,8 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
end
|
||||
end
|
||||
|
||||
@type fetch_actor_errors :: :json_decode_error | :actor_deleted | :http_error
|
||||
@type fetch_actor_errors ::
|
||||
:json_decode_error | :actor_deleted | :http_error | :actor_not_allowed_type
|
||||
|
||||
@doc """
|
||||
Fetching a remote actor's information through its AP ID
|
||||
@@ -130,7 +130,14 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
case Jason.decode(body) do
|
||||
{:ok, data} when is_map(data) ->
|
||||
Logger.debug("Got activity+json response at actor's endpoint, now converting data")
|
||||
{:ok, ActorConverter.as_to_model_data(data)}
|
||||
|
||||
case ActorConverter.as_to_model_data(data) do
|
||||
{:error, :actor_not_allowed_type} ->
|
||||
{:error, :actor_not_allowed_type}
|
||||
|
||||
map when is_map(map) ->
|
||||
{:ok, map}
|
||||
end
|
||||
|
||||
{:error, %Jason.DecodeError{} = e} ->
|
||||
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
@@ -164,12 +171,7 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
|
||||
@spec address_valid?(String.t()) :: boolean
|
||||
defp address_valid?(address) do
|
||||
case URI.parse(address) do
|
||||
%URI{host: host, scheme: scheme} ->
|
||||
is_valid_string(host) and is_valid_string(scheme)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
%URI{host: host, scheme: scheme} = URI.parse(address)
|
||||
is_valid_string(host) and is_valid_string(scheme)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,21 +26,25 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
Relay.get_actor()
|
||||
end
|
||||
|
||||
with :ok <- fetch_group(url, on_behalf_of) do
|
||||
{:ok, group}
|
||||
case fetch_group(url, on_behalf_of) do
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
:ok ->
|
||||
{:ok, group}
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_profile(%Actor{type: type, url: url}) when type in [:Person, :Application] do
|
||||
case ActivityPubActor.make_actor_from_url(url) do
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
{:ok, %Actor{outbox_url: outbox_url} = actor} ->
|
||||
case fetch_collection(outbox_url, Relay.get_actor()) do
|
||||
:ok -> {:ok, actor}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,6 +53,11 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
@spec fetch_group(String.t(), Actor.t()) :: :ok | {:error, fetch_actor_errors}
|
||||
def fetch_group(group_url, %Actor{} = on_behalf_of) do
|
||||
case ActivityPubActor.make_actor_from_url(group_url) do
|
||||
{:error, err}
|
||||
when err in [:actor_deleted, :http_error, :json_decode_error, :actor_is_local] ->
|
||||
Logger.debug("Error while making actor")
|
||||
{:error, err}
|
||||
|
||||
{:ok,
|
||||
%Actor{
|
||||
outbox_url: outbox_url,
|
||||
@@ -75,11 +84,6 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
Logger.debug("Error while fetching actor collection")
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, err}
|
||||
when err in [:actor_deleted, :http_error, :json_decode_error, :actor_is_local] ->
|
||||
Logger.debug("Error while making actor")
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -113,7 +117,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_element(String.t(), Actor.t()) :: any()
|
||||
@spec fetch_element(String.t(), Actor.t()) :: {:ok, struct()} | {:error, any()}
|
||||
def fetch_element(url, %Actor{} = on_behalf_of) do
|
||||
with {:ok, data} <- Fetcher.fetch(url, on_behalf_of: on_behalf_of) do
|
||||
case handling_element(data) do
|
||||
@@ -123,6 +127,9 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
{:ok, entity} ->
|
||||
{:ok, entity}
|
||||
|
||||
:error ->
|
||||
{:error, :err_fetching_element}
|
||||
|
||||
err ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
@@ -27,76 +27,100 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
||||
get_actor()
|
||||
end
|
||||
|
||||
@spec get_actor() :: Actor.t() | {:error, Ecto.Changeset.t()}
|
||||
@spec get_actor() :: Actor.t() | no_return
|
||||
def get_actor do
|
||||
with {:ok, %Actor{} = actor} <-
|
||||
Actors.get_or_create_internal_actor("relay") do
|
||||
actor
|
||||
case Actors.get_or_create_internal_actor("relay") do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
actor
|
||||
|
||||
{:error, %Ecto.Changeset{} = _err} ->
|
||||
raise("Relay actor not found")
|
||||
end
|
||||
end
|
||||
|
||||
@spec follow(String.t()) :: {:ok, Activity.t(), Follower.t()}
|
||||
@spec follow(String.t()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom()} | {:error, String.t()}
|
||||
def follow(address) do
|
||||
%Actor{} = local_actor = get_actor()
|
||||
|
||||
with {:ok, target_instance} <- fetch_actor(address),
|
||||
%Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <-
|
||||
ActivityPubActor.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
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while following remote instance: #{inspect(e)}")
|
||||
{:error, e}
|
||||
{:error, :person_no_follow} ->
|
||||
Logger.warn("Only group and instances can be followed")
|
||||
{:error, :person_no_follow}
|
||||
|
||||
e ->
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while following remote instance: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@spec unfollow(String.t()) :: {:ok, Activity.t(), Follower.t()}
|
||||
@spec unfollow(String.t()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom()} | {:error, String.t()}
|
||||
def unfollow(address) do
|
||||
%Actor{} = local_actor = get_actor()
|
||||
|
||||
with {:ok, target_instance} <- fetch_actor(address),
|
||||
%Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <-
|
||||
ActivityPubActor.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 ->
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while unfollowing remote instance: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept(String.t()) :: {:ok, Activity.t(), Follower.t()}
|
||||
@spec accept(String.t()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom()} | {:error, String.t()}
|
||||
def accept(address) do
|
||||
Logger.debug("We're trying to accept a relay subscription")
|
||||
%Actor{} = local_actor = get_actor()
|
||||
|
||||
with {:ok, target_instance} <- fetch_actor(address),
|
||||
%Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(target_instance),
|
||||
{:ok, activity, follow} <- Follows.accept(target_actor, local_actor) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while accepting remote instance follow: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject(String.t()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom()} | {:error, String.t()}
|
||||
def reject(address) do
|
||||
Logger.debug("We're trying to reject a relay subscription")
|
||||
%Actor{} = local_actor = get_actor()
|
||||
|
||||
with {:ok, target_instance} <- fetch_actor(address),
|
||||
%Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(target_instance),
|
||||
{:ok, activity, follow} <- Follows.reject(target_actor, local_actor) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while rejecting remote instance follow: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@spec refresh(String.t()) :: {:ok, any()}
|
||||
@spec refresh(String.t()) ::
|
||||
{:ok, Oban.Job.t()}
|
||||
| {:error, Ecto.Changeset.t()}
|
||||
| {:error, :bad_url}
|
||||
| {:error, Mobilizon.Federation.ActivityPub.Actor.make_actor_errors()}
|
||||
| {:error, :no_internal_relay_actor}
|
||||
| {:error, :url_nil}
|
||||
def refresh(address) do
|
||||
Logger.debug("We're trying to refresh a remote instance")
|
||||
|
||||
@@ -106,6 +130,10 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
||||
Background.enqueue("refresh_profile", %{
|
||||
"actor_id" => target_actor_id
|
||||
})
|
||||
else
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while refreshing remote instance: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:existing_comment, {:ok, %Comment{} = comment}} ->
|
||||
{:ok, nil, comment}
|
||||
|
||||
{:error, :event_comments_are_closed} ->
|
||||
{:error, :event_not_allow_commenting} ->
|
||||
Logger.debug("Tried to reply to an event for which comments are closed")
|
||||
:error
|
||||
end
|
||||
@@ -210,7 +210,11 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
{:error, :person_no_follow} ->
|
||||
Logger.warn("Only group and instances can be followed")
|
||||
:error
|
||||
|
||||
{:error, e} ->
|
||||
Logger.warn("Unable to handle Follow activity #{inspect(e)}")
|
||||
:error
|
||||
end
|
||||
@@ -578,6 +582,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
def handle_incoming(
|
||||
%{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data
|
||||
) do
|
||||
Logger.info("Handle incoming to delete an object")
|
||||
|
||||
with actor_url <- Utils.get_actor(data),
|
||||
{:actor, {:ok, %Actor{} = actor}} <-
|
||||
{:actor, ActivityPubActor.get_or_fetch_actor_by_url(actor_url)},
|
||||
@@ -594,7 +600,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
Logger.warn("Object origin check failed")
|
||||
:error
|
||||
|
||||
{:actor, {:error, "Could not fetch by AP id"}} ->
|
||||
{:actor, {:error, _err}} ->
|
||||
{:error, :unknown_actor}
|
||||
|
||||
{:error, e} ->
|
||||
@@ -993,7 +999,6 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
end
|
||||
|
||||
# Comment initiates a whole discussion only if it has full title
|
||||
@spec is_data_for_comment_or_discussion?(map()) :: boolean()
|
||||
defp is_data_a_discussion_initialization?(object_data) do
|
||||
not Map.has_key?(object_data, :title) or
|
||||
is_nil(object_data.title) or object_data.title == ""
|
||||
@@ -1107,22 +1112,22 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
end
|
||||
|
||||
defp is_group_object_gone(object_id) do
|
||||
case ActivityPub.fetch_object_from_url(object_id, force: true) do
|
||||
{:error, error_message, object} when error_message in [:http_gone, :http_not_found] ->
|
||||
{:ok, object}
|
||||
Logger.debug("is_group_object_gone #{object_id}")
|
||||
|
||||
case ActivityPub.fetch_object_from_url(object_id, force: true) do
|
||||
# comments are just emptied
|
||||
{:ok, %Comment{deleted_at: deleted_at} = object} when not is_nil(deleted_at) ->
|
||||
{:ok, object}
|
||||
|
||||
{:error, :http_gone, object} ->
|
||||
Logger.debug("object is really gone")
|
||||
{:ok, object}
|
||||
|
||||
{:ok, %{url: url} = object} ->
|
||||
if Utils.are_same_origin?(url, Endpoint.url()),
|
||||
do: {:ok, object},
|
||||
else: {:error, "Group object URL remote"}
|
||||
|
||||
{:error, {:error, err}} ->
|
||||
{:error, err}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
|
||||
@@ -18,40 +18,49 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, Actor.t(), ActivityStream.t()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Actor.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args_for_actor(args),
|
||||
{:ok, %Actor{} = actor} <- Actors.create_actor(args),
|
||||
{:ok, _} <-
|
||||
GroupActivity.insert_activity(actor,
|
||||
subject: "group_created",
|
||||
actor_id: args.creator_actor_id
|
||||
),
|
||||
actor_as_data <- Convertible.model_to_as(actor),
|
||||
audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(actor_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, actor, create_data}
|
||||
args = prepare_args_for_actor(args)
|
||||
|
||||
case Actors.create_actor(args) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
GroupActivity.insert_activity(actor,
|
||||
subject: "group_created",
|
||||
actor_id: args.creator_actor_id
|
||||
)
|
||||
|
||||
actor_as_data = Convertible.model_to_as(actor)
|
||||
audience = %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []}
|
||||
create_data = make_create_data(actor_as_data, Map.merge(audience, additional))
|
||||
{:ok, actor, create_data}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Actor.t(), map, map) :: {:ok, Actor.t(), ActivityStream.t()}
|
||||
@spec update(Actor.t(), map, map) ::
|
||||
{:ok, Actor.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(%Actor{} = old_actor, args, additional) do
|
||||
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
|
||||
{:ok, _} <-
|
||||
GroupActivity.insert_activity(new_actor,
|
||||
subject: "group_updated",
|
||||
old_group: old_actor,
|
||||
updater_actor: Map.get(args, :updater_actor)
|
||||
),
|
||||
actor_as_data <- Convertible.model_to_as(new_actor),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"),
|
||||
audience <-
|
||||
Audience.get_audience(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}
|
||||
case Actors.update_actor(old_actor, args) do
|
||||
{:ok, %Actor{} = new_actor} ->
|
||||
GroupActivity.insert_activity(new_actor,
|
||||
subject: "group_updated",
|
||||
old_group: old_actor,
|
||||
updater_actor: Map.get(args, :updater_actor)
|
||||
)
|
||||
|
||||
actor_as_data = Convertible.model_to_as(new_actor)
|
||||
Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}")
|
||||
audience = Audience.get_audience(new_actor)
|
||||
additional = Map.merge(additional, %{"actor" => old_actor.url})
|
||||
update_data = make_update_data(actor_as_data, Map.merge(audience, additional))
|
||||
{:ok, new_actor, update_data}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -92,21 +101,24 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
|
||||
suspension = Map.get(additionnal, :suspension, false)
|
||||
|
||||
with {:ok, %Oban.Job{}} <-
|
||||
Actors.delete_actor(target_actor,
|
||||
# We completely delete the actor if the actor is remote
|
||||
reserve_username: is_nil(domain),
|
||||
suspension: suspension,
|
||||
author_id: author_id
|
||||
) do
|
||||
{:ok, activity_data, actor, target_actor}
|
||||
case Actors.delete_actor(target_actor,
|
||||
# We completely delete the actor if the actor is remote
|
||||
reserve_username: is_nil(domain),
|
||||
suspension: suspension,
|
||||
author_id: author_id
|
||||
) do
|
||||
{:ok, %Oban.Job{}} ->
|
||||
{:ok, activity_data, actor, target_actor}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec actor(Actor.t()) :: Actor.t() | nil
|
||||
def actor(%Actor{} = actor), do: actor
|
||||
|
||||
@spec actor(Actor.t()) :: Actor.t() | nil
|
||||
@spec group_actor(Actor.t()) :: Actor.t() | nil
|
||||
def group_actor(%Actor{} = actor), do: actor
|
||||
|
||||
@spec permissions(Actor.t()) :: Permission.t()
|
||||
@@ -121,59 +133,70 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
|
||||
@spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, ActivityStreams.t(), Member.t()}
|
||||
def join(%Actor{type: :Group} = group, %Actor{} = actor, _local, additional) do
|
||||
with role <-
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.get(:role, Mobilizon.Actors.get_default_member_role(group)),
|
||||
{:ok, %Member{} = member} <-
|
||||
Mobilizon.Actors.create_member(%{
|
||||
role: role,
|
||||
parent_id: group.id,
|
||||
actor_id: actor.id,
|
||||
url: Map.get(additional, :url),
|
||||
metadata:
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
||||
}),
|
||||
{:ok, _} <-
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_joined"),
|
||||
Absinthe.Subscription.publish(Endpoint, actor,
|
||||
group_membership_changed: [Actor.preferred_username_and_domain(group), actor.id]
|
||||
),
|
||||
join_data <- %{
|
||||
"type" => "Join",
|
||||
"id" => member.url,
|
||||
"actor" => actor.url,
|
||||
"object" => group.url
|
||||
},
|
||||
audience <-
|
||||
Audience.get_audience(member) do
|
||||
approve_if_default_role_is_member(
|
||||
group,
|
||||
actor,
|
||||
Map.merge(join_data, audience),
|
||||
member,
|
||||
role
|
||||
)
|
||||
role =
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.get(:role, Mobilizon.Actors.get_default_member_role(group))
|
||||
|
||||
case Mobilizon.Actors.create_member(%{
|
||||
role: role,
|
||||
parent_id: group.id,
|
||||
actor_id: actor.id,
|
||||
url: Map.get(additional, :url),
|
||||
metadata:
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
||||
}) do
|
||||
{:ok, %Member{} = member} ->
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_joined")
|
||||
|
||||
Absinthe.Subscription.publish(Endpoint, actor,
|
||||
group_membership_changed: [Actor.preferred_username_and_domain(group), actor.id]
|
||||
)
|
||||
|
||||
join_data = %{
|
||||
"type" => "Join",
|
||||
"id" => member.url,
|
||||
"actor" => actor.url,
|
||||
"object" => group.url
|
||||
}
|
||||
|
||||
audience = Audience.get_audience(member)
|
||||
|
||||
approve_if_default_role_is_member(
|
||||
group,
|
||||
actor,
|
||||
Map.merge(join_data, audience),
|
||||
member,
|
||||
role
|
||||
)
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec follow(Actor.t(), Actor.t(), boolean, map) ::
|
||||
{:accept, any}
|
||||
| {:ok, ActivityStreams.t(), Follower.t()}
|
||||
| {:error, :no_person, String.t()}
|
||||
| {:error,
|
||||
:person_no_follow | :already_following | :followed_suspended | Ecto.Changeset.t()}
|
||||
def follow(%Actor{} = follower_actor, %Actor{type: type} = followed, _local, additional)
|
||||
when type != :Person do
|
||||
with {:ok, %Follower{} = follower} <-
|
||||
Mobilizon.Actors.follow(followed, follower_actor, additional["activity_id"], false),
|
||||
:ok <- FollowMailer.send_notification_to_admins(follower),
|
||||
follower_as_data <- Convertible.model_to_as(follower) do
|
||||
approve_if_manually_approves_followers(follower, follower_as_data)
|
||||
case Mobilizon.Actors.follow(followed, follower_actor, additional["activity_id"], false) do
|
||||
{:ok, %Follower{} = follower} ->
|
||||
FollowMailer.send_notification_to_admins(follower)
|
||||
follower_as_data = Convertible.model_to_as(follower)
|
||||
approve_if_manually_approves_followers(follower, follower_as_data)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def follow(_, _, _, _), do: {:error, :no_person, "Only group and instances can be followed"}
|
||||
# "Only group and instances can be followed"
|
||||
def follow(_, _, _, _), do: {:error, :person_no_follow}
|
||||
|
||||
@spec prepare_args_for_actor(map) :: map
|
||||
defp prepare_args_for_actor(args) do
|
||||
@@ -242,7 +265,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
end
|
||||
end
|
||||
|
||||
@spec approve_if_manually_approves_followers(Follower.t(), ActivityStreams.t()) ::
|
||||
@spec approve_if_manually_approves_followers(
|
||||
follower :: Follower.t(),
|
||||
follow_as_data :: ActivityStreams.t()
|
||||
) ::
|
||||
{:accept, any} | {:ok, ActivityStreams.t(), Follower.t()}
|
||||
defp approve_if_manually_approves_followers(
|
||||
%Follower{} = follower,
|
||||
|
||||
@@ -21,48 +21,56 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
||||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, Comment.t(), ActivityStream.t()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Comment.t(), ActivityStream.t()}
|
||||
| {:error, Ecto.Changeset.t()}
|
||||
| {:error, :event_not_allow_commenting}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args_for_comment(args),
|
||||
:ok <- make_sure_event_allows_commenting(args),
|
||||
{:ok, %Comment{discussion_id: discussion_id} = comment} <-
|
||||
Discussions.create_comment(args),
|
||||
{:ok, _} <-
|
||||
CommentActivity.insert_activity(comment,
|
||||
subject: "comment_posted"
|
||||
),
|
||||
:ok <- maybe_publish_graphql_subscription(discussion_id),
|
||||
comment_as_data <- Convertible.model_to_as(comment),
|
||||
audience <-
|
||||
Audience.get_audience(comment),
|
||||
create_data <-
|
||||
make_create_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, comment, create_data}
|
||||
args = prepare_args_for_comment(args)
|
||||
|
||||
if event_allows_commenting?(args) do
|
||||
case Discussions.create_comment(args) do
|
||||
{:ok, %Comment{discussion_id: discussion_id} = comment} ->
|
||||
CommentActivity.insert_activity(comment,
|
||||
subject: "comment_posted"
|
||||
)
|
||||
|
||||
maybe_publish_graphql_subscription(discussion_id)
|
||||
comment_as_data = Convertible.model_to_as(comment)
|
||||
audience = Audience.get_audience(comment)
|
||||
create_data = make_create_data(comment_as_data, Map.merge(audience, additional))
|
||||
{:ok, comment, create_data}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
else
|
||||
{:error, :event_not_allow_commenting}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), ActivityStream.t()}
|
||||
@spec update(Comment.t(), map(), map()) ::
|
||||
{:ok, Comment.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(%Comment{} = old_comment, args, additional) do
|
||||
with args <- prepare_args_for_comment_update(args),
|
||||
{:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
|
||||
comment_as_data <- Convertible.model_to_as(new_comment),
|
||||
audience <-
|
||||
Audience.get_audience(new_comment),
|
||||
update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_comment, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
args = prepare_args_for_comment_update(args)
|
||||
|
||||
case Discussions.update_comment(old_comment, args) do
|
||||
{:ok, %Comment{} = new_comment} ->
|
||||
{:ok, true} = Cachex.del(:activity_pub, "comment_#{new_comment.uuid}")
|
||||
comment_as_data = Convertible.model_to_as(new_comment)
|
||||
audience = Audience.get_audience(new_comment)
|
||||
update_data = make_update_data(comment_as_data, Map.merge(audience, additional))
|
||||
{:ok, new_comment, update_data}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Comment.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Comment.t()}
|
||||
{:ok, ActivityStream.t(), Actor.t(), Comment.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(
|
||||
%Comment{url: url, id: comment_id},
|
||||
%Actor{} = actor,
|
||||
@@ -81,15 +89,17 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
||||
|
||||
force_deletion = Map.get(options, :force, false)
|
||||
|
||||
with audience <-
|
||||
Audience.get_audience(comment),
|
||||
{:ok, %Comment{} = updated_comment} <-
|
||||
Discussions.delete_comment(comment, force: force_deletion),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}) do
|
||||
Share.delete_all_by_uri(comment.url)
|
||||
{:ok, Map.merge(activity_data, audience), actor, updated_comment}
|
||||
audience = Audience.get_audience(comment)
|
||||
|
||||
case Discussions.delete_comment(comment, force: force_deletion) do
|
||||
{:ok, %Comment{} = updated_comment} ->
|
||||
Cachex.del(:activity_pub, "comment_#{comment.uuid}")
|
||||
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id})
|
||||
Share.delete_all_by_uri(comment.url)
|
||||
{:ok, Map.merge(activity_data, audience), actor, updated_comment}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -185,31 +195,31 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
||||
defp maybe_publish_graphql_subscription(nil), do: :ok
|
||||
|
||||
defp maybe_publish_graphql_subscription(discussion_id) do
|
||||
with %Discussion{} = discussion <- Discussions.get_discussion(discussion_id) do
|
||||
Absinthe.Subscription.publish(Endpoint, discussion,
|
||||
discussion_comment_changed: discussion.slug
|
||||
)
|
||||
case Discussions.get_discussion(discussion_id) do
|
||||
%Discussion{} = discussion ->
|
||||
Absinthe.Subscription.publish(Endpoint, discussion,
|
||||
discussion_comment_changed: discussion.slug
|
||||
)
|
||||
|
||||
:ok
|
||||
:ok
|
||||
|
||||
nil ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@spec make_sure_event_allows_commenting(%{actor_id: String.t() | integer, event: Event.t()}) ::
|
||||
:ok | {:error, :event_comments_are_closed}
|
||||
defp make_sure_event_allows_commenting(%{
|
||||
@spec event_allows_commenting?(%{actor_id: String.t() | integer, event: Event.t()}) :: boolean
|
||||
defp event_allows_commenting?(%{
|
||||
actor_id: actor_id,
|
||||
event: %Event{
|
||||
options: %EventOptions{comment_moderation: comment_moderation},
|
||||
organizer_actor_id: organizer_actor_id
|
||||
}
|
||||
}) do
|
||||
if comment_moderation != :closed ||
|
||||
to_string(actor_id) == to_string(organizer_actor_id) do
|
||||
:ok
|
||||
else
|
||||
{:error, :event_comments_are_closed}
|
||||
end
|
||||
comment_moderation != :closed ||
|
||||
to_string(actor_id) == to_string(organizer_actor_id)
|
||||
end
|
||||
|
||||
defp make_sure_event_allows_commenting(_), do: :ok
|
||||
# Comments not attached to events
|
||||
defp event_allows_commenting?(_), do: true
|
||||
end
|
||||
|
||||
@@ -43,13 +43,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
|
||||
| TodoList.t()
|
||||
|
||||
@callback create(data :: any(), additionnal :: map()) ::
|
||||
{:ok, t(), ActivityStream.t()}
|
||||
{:ok, t(), ActivityStream.t()} | {:error, any()}
|
||||
|
||||
@callback update(struct :: t(), attrs :: map(), additionnal :: map()) ::
|
||||
{:ok, t(), ActivityStream.t()}
|
||||
{:ok, t(), ActivityStream.t()} | {:error, any()}
|
||||
|
||||
@callback delete(struct :: t(), Actor.t(), local :: boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), t()}
|
||||
{:ok, ActivityStream.t(), Actor.t(), t()} | {:error, any()}
|
||||
end
|
||||
|
||||
defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
|
||||
@@ -57,14 +57,15 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
|
||||
ActivityPub entity Managable protocol.
|
||||
"""
|
||||
|
||||
@spec update(Entity.t(), map(), map()) :: {:ok, Entity.t(), ActivityStream.t()}
|
||||
@spec update(Entity.t(), map(), map()) ::
|
||||
{:ok, Entity.t(), ActivityStream.t()} | {:error, any()}
|
||||
@doc """
|
||||
Updates a `Managable` entity with the appropriate attributes and returns the updated entity and an activitystream representation for it
|
||||
"""
|
||||
def update(entity, attrs, additionnal)
|
||||
|
||||
@spec delete(Entity.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Entity.t()}
|
||||
{:ok, ActivityStream.t(), Actor.t(), Entity.t()} | {:error, any()}
|
||||
@doc "Deletes an entity and returns the activitystream representation for it"
|
||||
def delete(entity, actor, local, additionnal)
|
||||
end
|
||||
|
||||
@@ -22,45 +22,53 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, Event.t(), ActivityStream.t()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Event.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args_for_event(args),
|
||||
{:ok, %Event{} = event} <- EventsManager.create_event(args),
|
||||
{:ok, _} <-
|
||||
EventActivity.insert_activity(event, subject: "event_created"),
|
||||
event_as_data <- Convertible.model_to_as(event),
|
||||
audience <-
|
||||
Audience.get_audience(event),
|
||||
create_data <-
|
||||
make_create_data(event_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, event, create_data}
|
||||
args = prepare_args_for_event(args)
|
||||
|
||||
case EventsManager.create_event(args) do
|
||||
{:ok, %Event{} = event} ->
|
||||
EventActivity.insert_activity(event, subject: "event_created")
|
||||
event_as_data = Convertible.model_to_as(event)
|
||||
audience = Audience.get_audience(event)
|
||||
create_data = make_create_data(event_as_data, Map.merge(audience, additional))
|
||||
{:ok, event, create_data}
|
||||
|
||||
{:error, _step, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Event.t(), map(), map()) :: {:ok, Event.t(), ActivityStream.t()}
|
||||
@spec update(Event.t(), map(), map()) ::
|
||||
{:ok, Event.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(%Event{} = old_event, args, additional) do
|
||||
with args <- prepare_args_for_event(args),
|
||||
{:ok, %Event{} = new_event} <- EventsManager.update_event(old_event, args),
|
||||
{:ok, _} <-
|
||||
EventActivity.insert_activity(new_event, subject: "event_updated"),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"),
|
||||
event_as_data <- Convertible.model_to_as(new_event),
|
||||
audience <-
|
||||
Audience.get_audience(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
|
||||
args = prepare_args_for_event(args)
|
||||
|
||||
case EventsManager.update_event(old_event, args) do
|
||||
{:ok, %Event{} = new_event} ->
|
||||
EventActivity.insert_activity(new_event, subject: "event_updated")
|
||||
Cachex.del(:activity_pub, "event_#{new_event.uuid}")
|
||||
event_as_data = Convertible.model_to_as(new_event)
|
||||
audience = Audience.get_audience(new_event)
|
||||
update_data = make_update_data(event_as_data, Map.merge(audience, additional))
|
||||
{:ok, new_event, update_data}
|
||||
|
||||
{:error, _step, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Event.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Event.t()}
|
||||
{:ok, ActivityStream.t(), Actor.t(), Event.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(%Event{url: url} = event, %Actor{} = actor, _local, _additionnal) do
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
@@ -70,16 +78,23 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
"id" => url <> "/delete"
|
||||
}
|
||||
|
||||
with audience <-
|
||||
Audience.get_audience(event),
|
||||
{:ok, %Event{} = event} <- EventsManager.delete_event(event),
|
||||
{:ok, _} <-
|
||||
EventActivity.insert_activity(event, subject: "event_deleted"),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do
|
||||
Share.delete_all_by_uri(event.url)
|
||||
{:ok, Map.merge(activity_data, audience), actor, event}
|
||||
audience = Audience.get_audience(event)
|
||||
|
||||
case EventsManager.delete_event(event) do
|
||||
{:ok, %Event{} = event} ->
|
||||
case Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do
|
||||
{:ok, %Tombstone{} = _tombstone} ->
|
||||
EventActivity.insert_activity(event, subject: "event_deleted")
|
||||
Cachex.del(:activity_pub, "event_#{event.uuid}")
|
||||
Share.delete_all_by_uri(event.url)
|
||||
{:ok, Map.merge(activity_data, audience), actor, event}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -111,16 +126,16 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
|
||||
@spec join(Event.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, ActivityStreams.t(), Participant.t()}
|
||||
| {:accept, any()}
|
||||
| {:error, :maximum_attendee_capacity_reached}
|
||||
def join(%Event{} = event, %Actor{} = actor, _local, additional) do
|
||||
with {:maximum_attendee_capacity, true} <-
|
||||
{:maximum_attendee_capacity, check_attendee_capacity?(event)},
|
||||
role <-
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)),
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.create_participant(%{
|
||||
if check_attendee_capacity?(event) do
|
||||
role =
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event))
|
||||
|
||||
case Mobilizon.Events.create_participant(%{
|
||||
role: role,
|
||||
event_id: event.id,
|
||||
actor_id: actor.id,
|
||||
@@ -129,19 +144,23 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
||||
}),
|
||||
join_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
Audience.get_audience(participant) do
|
||||
approve_if_default_role_is_participant(
|
||||
event,
|
||||
Map.merge(join_data, audience),
|
||||
participant,
|
||||
role
|
||||
)
|
||||
}) do
|
||||
{:ok, %Participant{} = participant} ->
|
||||
join_data = Convertible.model_to_as(participant)
|
||||
audience = Audience.get_audience(participant)
|
||||
|
||||
approve_if_default_role_is_participant(
|
||||
event,
|
||||
Map.merge(join_data, audience),
|
||||
participant,
|
||||
role
|
||||
)
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
else
|
||||
{:maximum_attendee_capacity, false} ->
|
||||
{:error, :maximum_attendee_capacity_reached}
|
||||
{:error, :maximum_attendee_capacity_reached}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -160,7 +179,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
ActivityStreams.t(),
|
||||
Participant.t(),
|
||||
ParticipantRole.t()
|
||||
) :: {:ok, ActivityStreams.t(), Participant.t()}
|
||||
) :: {:ok, ActivityStreams.t(), Participant.t()} | {:accept, any()}
|
||||
defp approve_if_default_role_is_participant(event, activity_data, participant, role) do
|
||||
case event do
|
||||
%Event{attributed_to: %Actor{id: group_id, url: group_url}} ->
|
||||
@@ -175,12 +194,12 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
{:ok, activity_data, participant}
|
||||
end
|
||||
|
||||
%Event{local: true} ->
|
||||
%Event{attributed_to: nil, local: true} ->
|
||||
do_approve(event, activity_data, participant, role, %{
|
||||
"actor" => event.organizer_actor.url
|
||||
})
|
||||
|
||||
_ ->
|
||||
%Event{} ->
|
||||
{:ok, activity_data, participant}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
|
||||
require Logger
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2]
|
||||
|
||||
@spec update(Member.t(), map, map) :: {:ok, Member.t(), ActivityStream.t()}
|
||||
@spec update(Member.t(), map, map) ::
|
||||
{:ok, Member.t(), ActivityStream.t()}
|
||||
| {:error, :member_not_found | :only_admin_left | Ecto.Changeset.t()}
|
||||
def update(
|
||||
%Member{
|
||||
parent: %Actor{id: group_id} = group,
|
||||
@@ -20,39 +22,46 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
|
||||
%{role: updated_role} = args,
|
||||
%{moderator: %Actor{url: moderator_url, id: moderator_id} = moderator} = additional
|
||||
) do
|
||||
with additional <- Map.delete(additional, :moderator),
|
||||
{:has_rights_to_update_role, {:ok, %Member{role: moderator_role}}}
|
||||
when moderator_role in [:moderator, :administrator, :creator] <-
|
||||
{:has_rights_to_update_role, Actors.get_member(moderator_id, group_id)},
|
||||
{:is_only_admin, false} <-
|
||||
{:is_only_admin, check_admins_left?(member_id, group_id, current_role, updated_role)},
|
||||
{:ok, %Member{} = member} <-
|
||||
Actors.update_member(old_member, args),
|
||||
{:ok, _} <-
|
||||
MemberActivity.insert_activity(member,
|
||||
old_member: old_member,
|
||||
moderator: moderator,
|
||||
subject: "member_updated"
|
||||
),
|
||||
Absinthe.Subscription.publish(Endpoint, actor,
|
||||
group_membership_changed: [Actor.preferred_username_and_domain(group), actor_id]
|
||||
),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "member_#{member_id}"),
|
||||
member_as_data <-
|
||||
Convertible.model_to_as(member),
|
||||
audience <- %{
|
||||
"to" => [member.parent.members_url, member.actor.url],
|
||||
"cc" => [member.parent.url],
|
||||
"actor" => moderator_url,
|
||||
"attributedTo" => [member.parent.url]
|
||||
} do
|
||||
update_data = make_update_data(member_as_data, Map.merge(audience, additional))
|
||||
additional = Map.delete(additional, :moderator)
|
||||
|
||||
{:ok, member, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
case Actors.get_member(moderator_id, group_id) do
|
||||
{:error, :member_not_found} ->
|
||||
{:error, :member_not_found}
|
||||
|
||||
{:ok, %Member{role: moderator_role}}
|
||||
when moderator_role in [:moderator, :administrator, :creator] ->
|
||||
if check_admins_left?(member_id, group_id, current_role, updated_role) do
|
||||
{:error, :only_admin_left}
|
||||
else
|
||||
case Actors.update_member(old_member, args) do
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
|
||||
{:ok, %Member{} = member} ->
|
||||
MemberActivity.insert_activity(member,
|
||||
old_member: old_member,
|
||||
moderator: moderator,
|
||||
subject: "member_updated"
|
||||
)
|
||||
|
||||
Absinthe.Subscription.publish(Endpoint, actor,
|
||||
group_membership_changed: [Actor.preferred_username_and_domain(group), actor_id]
|
||||
)
|
||||
|
||||
Cachex.del(:activity_pub, "member_#{member_id}")
|
||||
member_as_data = Convertible.model_to_as(member)
|
||||
|
||||
audience = %{
|
||||
"to" => [member.parent.members_url, member.actor.url],
|
||||
"cc" => [member.parent.url],
|
||||
"actor" => moderator_url,
|
||||
"attributedTo" => [member.parent.url]
|
||||
}
|
||||
|
||||
update_data = make_update_data(member_as_data, Map.merge(audience, additional))
|
||||
{:ok, member, update_data}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
@doc """
|
||||
Enqueues an activity for federation if it's local
|
||||
"""
|
||||
@spec maybe_federate(activity :: Activity.t()) :: :ok
|
||||
def maybe_federate(%Activity{local: true} = activity) do
|
||||
Logger.debug("Maybe federate an activity")
|
||||
|
||||
@@ -165,12 +166,12 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
Logger.info("Forwarded activity to external members of the group")
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
{:error, _err} ->
|
||||
Logger.info("Failed to forward activity to external members of the group")
|
||||
:error
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -311,7 +312,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
{:ok, media} ->
|
||||
media
|
||||
|
||||
_ ->
|
||||
{:error, _err} ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -509,7 +510,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
@doc """
|
||||
Make add activity data
|
||||
"""
|
||||
@spec make_add_data(map(), map()) :: map()
|
||||
@spec make_add_data(map(), map(), map()) :: map()
|
||||
def make_add_data(object, target, additional \\ %{}) do
|
||||
Logger.debug("Making add data")
|
||||
Logger.debug(inspect(object))
|
||||
@@ -530,7 +531,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
@doc """
|
||||
Make move activity data
|
||||
"""
|
||||
@spec make_add_data(map(), map()) :: map()
|
||||
@spec make_move_data(map(), map(), map(), map()) :: map()
|
||||
def make_move_data(object, origin, target, additional \\ %{}) do
|
||||
Logger.debug("Making move data")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
@@ -38,52 +38,49 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: map | {:error, any()}
|
||||
@spec as_to_model_data(map) :: map | {:error, atom}
|
||||
def as_to_model_data(object) do
|
||||
Logger.debug("We're converting raw ActivityStream data to a comment entity")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <-
|
||||
maybe_fetch_actor_and_attributed_to_id(object),
|
||||
{:tags, tags} <- {:tags, fetch_tags(Map.get(object, "tag", []))},
|
||||
{:mentions, mentions} <-
|
||||
{:mentions, fetch_mentions(Map.get(object, "tag", []))},
|
||||
discussion <-
|
||||
Discussions.get_discussion_by_url(Map.get(object, "context")) do
|
||||
Logger.debug("Inserting full comment")
|
||||
Logger.debug(inspect(object))
|
||||
tag_object = Map.get(object, "tag", [])
|
||||
|
||||
data = %{
|
||||
text: object["content"],
|
||||
url: object["id"],
|
||||
# Will be used in conversations, ignored in basic comments
|
||||
title: object["name"],
|
||||
context: object["context"],
|
||||
actor_id: actor_id,
|
||||
attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id),
|
||||
in_reply_to_comment_id: nil,
|
||||
event_id: nil,
|
||||
uuid: object["uuid"],
|
||||
discussion_id: if(is_nil(discussion), do: nil, else: discussion.id),
|
||||
tags: tags,
|
||||
mentions: mentions,
|
||||
local: is_nil(actor_domain),
|
||||
visibility: if(Visibility.is_public?(object), do: :public, else: :private),
|
||||
published_at: object["published"],
|
||||
is_announcement: Map.get(object, "isAnnouncement", false)
|
||||
}
|
||||
case maybe_fetch_actor_and_attributed_to_id(object) do
|
||||
{:ok, %Actor{id: actor_id, domain: actor_domain}, attributed_to} ->
|
||||
Logger.debug("Inserting full comment")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
Logger.debug("Converted object before fetching parents")
|
||||
Logger.debug(inspect(data))
|
||||
data = %{
|
||||
text: object["content"],
|
||||
url: object["id"],
|
||||
# Will be used in conversations, ignored in basic comments
|
||||
title: object["name"],
|
||||
context: object["context"],
|
||||
actor_id: actor_id,
|
||||
attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id),
|
||||
in_reply_to_comment_id: nil,
|
||||
event_id: nil,
|
||||
uuid: object["uuid"],
|
||||
discussion_id: get_discussion_id(object),
|
||||
tags: fetch_tags(tag_object),
|
||||
mentions: fetch_mentions(tag_object),
|
||||
local: is_nil(actor_domain),
|
||||
visibility: if(Visibility.is_public?(object), do: :public, else: :private),
|
||||
published_at: object["published"],
|
||||
is_announcement: Map.get(object, "isAnnouncement", false)
|
||||
}
|
||||
|
||||
data = maybe_fetch_parent_object(object, data)
|
||||
Logger.debug("Converted object before fetching parents")
|
||||
Logger.debug(inspect(data))
|
||||
|
||||
Logger.debug("Converted object after fetching parents")
|
||||
Logger.debug(inspect(data))
|
||||
data
|
||||
else
|
||||
{:ok, %Actor{suspended: true}} ->
|
||||
:error
|
||||
data = maybe_fetch_parent_object(object, data)
|
||||
|
||||
Logger.debug("Converted object after fetching parents")
|
||||
Logger.debug(inspect(data))
|
||||
data
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -94,9 +91,20 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(CommentModel.t()) :: map
|
||||
def model_to_as(%CommentModel{deleted_at: nil} = comment) do
|
||||
def model_to_as(
|
||||
%CommentModel{
|
||||
deleted_at: nil,
|
||||
attributed_to: attributed_to,
|
||||
actor: %Actor{url: comment_actor_url}
|
||||
} = comment
|
||||
) do
|
||||
to = determine_to(comment)
|
||||
|
||||
attributed_to =
|
||||
if is_nil(attributed_to),
|
||||
do: comment_actor_url,
|
||||
else: Map.get(attributed_to, :url, comment_actor_url)
|
||||
|
||||
object = %{
|
||||
"type" => "Note",
|
||||
"to" => to,
|
||||
@@ -104,9 +112,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
"content" => comment.text,
|
||||
"mediaType" => "text/html",
|
||||
"actor" => comment.actor.url,
|
||||
"attributedTo" =>
|
||||
if(is_nil(comment.attributed_to), do: nil, else: comment.attributed_to.url) ||
|
||||
comment.actor.url,
|
||||
"attributedTo" => attributed_to,
|
||||
"uuid" => comment.uuid,
|
||||
"id" => comment.url,
|
||||
"tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags),
|
||||
@@ -132,7 +138,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
end
|
||||
|
||||
@impl Converter
|
||||
@spec model_to_as(CommentModel.t()) :: map
|
||||
def model_to_as(%CommentModel{} = comment) do
|
||||
Convertible.model_to_as(%TombstoneModel{
|
||||
uri: comment.url,
|
||||
@@ -203,4 +208,13 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
defp get_discussion_id(%{"context" => context}) do
|
||||
case Discussions.get_discussion_by_url(context) do
|
||||
%Discussion{id: discussion_id} -> discussion_id
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp get_discussion_id(_object), do: nil
|
||||
end
|
||||
|
||||
@@ -47,7 +47,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: map() | {:error, any()} | :error
|
||||
def as_to_model_data(object) do
|
||||
with {%Actor{id: actor_id}, attributed_to} <-
|
||||
with {:ok, %Actor{id: actor_id}, attributed_to} <-
|
||||
maybe_fetch_actor_and_attributed_to_id(object),
|
||||
{:address, address_id} <-
|
||||
{:address, get_address(object["location"])},
|
||||
@@ -87,7 +87,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
language: object["inLanguage"]
|
||||
}
|
||||
else
|
||||
{:ok, %Actor{suspended: true}} ->
|
||||
{:error, _err} ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Medias
|
||||
alias Mobilizon.Medias.Media, as: MediaModel
|
||||
|
||||
@@ -18,7 +19,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
|
||||
@doc """
|
||||
Convert a media struct to an ActivityStream representation.
|
||||
"""
|
||||
@spec model_to_as(MediaModel.t()) :: map
|
||||
@spec model_to_as(MediaModel.t()) :: ActivityStream.t()
|
||||
def model_to_as(%MediaModel{file: file}) do
|
||||
%{
|
||||
"type" => "Document",
|
||||
@@ -31,29 +32,53 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
|
||||
@doc """
|
||||
Save media data from raw data and return AS Link data.
|
||||
"""
|
||||
@spec find_or_create_media(map(), String.t() | integer()) ::
|
||||
{:ok, MediaModel.t()} | {:error, atom() | String.t() | Ecto.Changeset.t()}
|
||||
def find_or_create_media(%{"type" => "Link", "href" => url}, actor_id),
|
||||
do: find_or_create_media(url, actor_id)
|
||||
do:
|
||||
find_or_create_media(
|
||||
%{"type" => "Document", "url" => url, "name" => "External media"},
|
||||
actor_id
|
||||
)
|
||||
|
||||
def find_or_create_media(
|
||||
%{"type" => "Document", "url" => media_url, "name" => name},
|
||||
actor_id
|
||||
)
|
||||
when is_binary(media_url) do
|
||||
with {:ok, %{body: body}} <- Tesla.get(media_url, opts: @http_options),
|
||||
{:ok, %{url: url} = uploaded} <-
|
||||
Upload.store(%{body: body, name: name}),
|
||||
{:media_exists, nil} <- {:media_exists, Medias.get_media_by_url(url)} do
|
||||
Medias.create_media(%{
|
||||
file: Map.take(uploaded, [:url, :name, :content_type, :size]),
|
||||
metadata: Map.take(uploaded, [:width, :height, :blurhash]),
|
||||
actor_id: actor_id
|
||||
})
|
||||
else
|
||||
{:media_exists, %MediaModel{file: _file} = media} ->
|
||||
{:ok, media}
|
||||
case upload_media(media_url, name) do
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
err ->
|
||||
err
|
||||
{:ok, %{url: url} = uploaded} ->
|
||||
case Medias.get_media_by_url(url) do
|
||||
%MediaModel{file: _file} = media ->
|
||||
{:ok, media}
|
||||
|
||||
nil ->
|
||||
Medias.create_media(%{
|
||||
file: Map.take(uploaded, [:url, :name, :content_type, :size]),
|
||||
metadata: Map.take(uploaded, [:width, :height, :blurhash]),
|
||||
actor_id: actor_id
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec upload_media(String.t(), String.t()) :: {:ok, map()} | {:error, atom() | String.t()}
|
||||
defp upload_media(media_url, name) do
|
||||
case Tesla.get(media_url, opts: @http_options) do
|
||||
{:ok, %{body: body}} ->
|
||||
case Upload.store(%{body: body, name: name}) do
|
||||
{:ok, %{url: _url} = uploaded} ->
|
||||
{:ok, uploaded}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,23 +51,31 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do
|
||||
def as_to_model_data(
|
||||
%{"type" => "Todo", "actor" => actor_url, "todoList" => todo_list_url} = object
|
||||
) do
|
||||
with {:ok, %Actor{id: creator_id} = _creator} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(actor_url),
|
||||
{:todo_list, %TodoList{id: todo_list_id}} <-
|
||||
{:todo_list, Todos.get_todo_list_by_url(todo_list_url)} do
|
||||
%{
|
||||
title: object["name"],
|
||||
status: object["status"],
|
||||
url: object["id"],
|
||||
todo_list_id: todo_list_id,
|
||||
creator_id: creator_id,
|
||||
published_at: object["published"]
|
||||
}
|
||||
else
|
||||
{:todo_list, nil} ->
|
||||
with {:ok, %TodoList{}} <- ActivityPub.fetch_object_from_url(todo_list_url) do
|
||||
as_to_model_data(object)
|
||||
case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
|
||||
{:ok, %Actor{id: creator_id} = _creator} ->
|
||||
case Todos.get_todo_list_by_url(todo_list_url) do
|
||||
%TodoList{id: todo_list_id} ->
|
||||
%{
|
||||
title: object["name"],
|
||||
status: object["status"],
|
||||
url: object["id"],
|
||||
todo_list_id: todo_list_id,
|
||||
creator_id: creator_id,
|
||||
published_at: object["published"]
|
||||
}
|
||||
|
||||
nil ->
|
||||
case ActivityPub.fetch_object_from_url(todo_list_url) do
|
||||
{:ok, %TodoList{}} ->
|
||||
as_to_model_data(object)
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -111,7 +111,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils 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}} <-
|
||||
@@ -128,22 +127,34 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
||||
create_mention(mention, acc)
|
||||
end
|
||||
|
||||
@spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
|
||||
@spec maybe_fetch_actor_and_attributed_to_id(map()) ::
|
||||
{:ok, Actor.t(), Actor.t() | nil} | {:error, atom()}
|
||||
def maybe_fetch_actor_and_attributed_to_id(%{
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => attributed_to_url
|
||||
})
|
||||
when is_nil(attributed_to_url) do
|
||||
{fetch_actor(actor_url), nil}
|
||||
case fetch_actor(actor_url) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor, nil}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
|
||||
def maybe_fetch_actor_and_attributed_to_id(%{
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => attributed_to_url
|
||||
})
|
||||
when is_nil(actor_url) do
|
||||
{fetch_actor(attributed_to_url), nil}
|
||||
case fetch_actor(attributed_to_url) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor, nil}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
# Only when both actor and attributedTo fields are both filled is when we can return both
|
||||
@@ -152,9 +163,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
||||
"attributedTo" => attributed_to_url
|
||||
})
|
||||
when actor_url != attributed_to_url do
|
||||
with actor <- fetch_actor(actor_url),
|
||||
attributed_to <- fetch_actor(attributed_to_url) do
|
||||
{actor, attributed_to}
|
||||
with {:ok, %Actor{} = actor} <- fetch_actor(actor_url),
|
||||
{:ok, %Actor{} = attributed_to} <- fetch_actor(attributed_to_url) do
|
||||
{:ok, actor, attributed_to}
|
||||
else
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -162,16 +176,25 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
||||
def maybe_fetch_actor_and_attributed_to_id(%{
|
||||
"attributedTo" => attributed_to_url
|
||||
}) do
|
||||
{fetch_actor(attributed_to_url), nil}
|
||||
case fetch_actor(attributed_to_url) do
|
||||
{:ok, %Actor{} = attributed_to} -> {:ok, attributed_to, nil}
|
||||
{:error, err} -> {:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_fetch_actor_and_attributed_to_id(_), do: {nil, nil}
|
||||
def maybe_fetch_actor_and_attributed_to_id(_), do: {:error, :no_actor_found}
|
||||
|
||||
@spec fetch_actor(String.t()) :: Actor.t()
|
||||
@spec fetch_actor(String.t()) :: {:ok, Actor.t()} | {:error, atom()}
|
||||
defp fetch_actor(actor_url) do
|
||||
with {:ok, %Actor{suspended: false} = actor} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
|
||||
actor
|
||||
case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
|
||||
{:ok, %Actor{suspended: false} = actor} ->
|
||||
{:ok, actor}
|
||||
|
||||
{:ok, %Actor{suspended: true} = _actor} ->
|
||||
{:error, :actor_suspended}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -203,12 +226,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
||||
|> Map.new()
|
||||
|
||||
picture_id =
|
||||
with banner when is_map(banner) <- get_banner_picture(attachements),
|
||||
{:ok, %Media{id: picture_id}} <-
|
||||
MediaConverter.find_or_create_media(banner, actor_id) do
|
||||
picture_id
|
||||
else
|
||||
_err ->
|
||||
case get_banner_picture(attachements) do
|
||||
banner when is_map(banner) ->
|
||||
case MediaConverter.find_or_create_media(banner, actor_id) do
|
||||
{:error, _err} ->
|
||||
nil
|
||||
|
||||
{:ok, %Media{id: picture_id}} ->
|
||||
picture_id
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
@@ -87,12 +87,16 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
||||
:ok <- Logger.debug("Fetching public key for #{actor_id}"),
|
||||
{:ok, public_key} <- get_public_key_for_url(actor_id) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec refetch_public_key(Plug.Conn.t()) ::
|
||||
{:ok, String.t()}
|
||||
| {:error, :actor_fetch_error | :actor_not_fetchable | :pem_decode_error}
|
||||
| {:error, :actor_fetch_error | :actor_not_fetchable | :pem_decode_error,
|
||||
:actor_is_local}
|
||||
def refetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
actor_id <- key_id_to_actor_url(kid),
|
||||
@@ -100,10 +104,13 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
||||
{:ok, _actor} <- ActivityPubActor.make_actor_from_url(actor_id),
|
||||
{:ok, public_key} <- get_public_key_for_url(actor_id) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec sign(Actor.t(), map()) :: String.t()
|
||||
@spec sign(Actor.t(), map()) :: String.t() | {:error, :pem_decode_error} | no_return
|
||||
def sign(%Actor{domain: domain, keys: keys} = actor, headers) when is_nil(domain) do
|
||||
Logger.debug("Signing a payload on behalf of #{actor.url}")
|
||||
Logger.debug("headers")
|
||||
|
||||
@@ -27,7 +27,7 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
base_url = Endpoint.url()
|
||||
%URI{host: host} = URI.parse(base_url)
|
||||
|
||||
{
|
||||
XmlBuilder.to_doc({
|
||||
:XRD,
|
||||
%{
|
||||
xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0",
|
||||
@@ -47,8 +47,7 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|> XmlBuilder.to_doc()
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -150,7 +149,7 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
|
||||
{:error, err} ->
|
||||
Logger.debug("Couldn't process webfinger data for #{actor}")
|
||||
err
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
@@ -187,7 +186,8 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`)
|
||||
# Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta`
|
||||
# to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`)
|
||||
@spec find_webfinger_endpoint(String.t()) ::
|
||||
{:ok, String.t()} | {:error, :link_not_found} | {:error, any()}
|
||||
defp find_webfinger_endpoint(domain) when is_binary(domain) do
|
||||
|
||||
@@ -5,42 +5,54 @@
|
||||
|
||||
defmodule Mobilizon.Federation.WebFinger.XmlBuilder do
|
||||
@moduledoc """
|
||||
Builds XRD for WebFinger host_meta.
|
||||
Extremely basic XML encoder. Builds XRD for WebFinger host_meta.
|
||||
"""
|
||||
|
||||
def to_xml({tag, attributes, content}) do
|
||||
@typep content :: list({tag :: atom(), attributes :: map()}) | String.t()
|
||||
@typep document :: {tag :: atom(), attributes :: map(), content :: content}
|
||||
|
||||
@doc """
|
||||
Return the XML representation for a document.
|
||||
"""
|
||||
@spec to_doc(document :: document) :: String.t()
|
||||
def to_doc(document), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(document)
|
||||
|
||||
@spec to_xml(document) :: String.t()
|
||||
@spec to_xml({tag :: atom(), attributes :: map()}) :: String.t()
|
||||
@spec to_xml({tag :: atom(), content :: content}) :: String.t()
|
||||
@spec to_xml(content :: content) :: String.t()
|
||||
defp 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
|
||||
defp 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})
|
||||
defp to_xml({tag, content}), do: to_xml({tag, %{}, content})
|
||||
|
||||
def to_xml(content) when is_binary(content), do: to_string(content)
|
||||
defp to_xml(content) when is_binary(content), do: to_string(content)
|
||||
|
||||
def to_xml(content) when is_list(content) do
|
||||
defp to_xml(content) when is_list(content) do
|
||||
content
|
||||
|> Enum.map(&to_xml/1)
|
||||
|> Enum.join()
|
||||
end
|
||||
|
||||
def to_xml(%NaiveDateTime{} = time), do: NaiveDateTime.to_iso8601(time)
|
||||
|
||||
def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(content)
|
||||
defp to_xml(%NaiveDateTime{} = time), do: NaiveDateTime.to_iso8601(time)
|
||||
|
||||
@spec make_open_tag(tag :: atom, attributes :: map()) :: String.t()
|
||||
defp make_open_tag(tag, attributes) do
|
||||
attributes_string =
|
||||
attributes
|
||||
|> Enum.map(fn {attribute, value} -> "#{attribute}=\"#{value}\"" end)
|
||||
|> Enum.join(" ")
|
||||
|
||||
[tag, attributes_string] |> Enum.join(" ") |> String.trim()
|
||||
[to_string(tag), attributes_string] |> Enum.join(" ") |> String.trim()
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user