Various refactoring and typespec improvements

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-09-24 16:46:42 +02:00
parent d235653876
commit 1893d9f55b
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
142 changed files with 1854 additions and 1297 deletions

View File

@ -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

View File

@ -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} ->

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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}