diff --git a/lib/federation/activity_pub/activity_pub.ex b/lib/federation/activity_pub/activity_pub.ex index a8b4203b..df1c5c4e 100644 --- a/lib/federation/activity_pub/activity_pub.ex +++ b/lib/federation/activity_pub/activity_pub.ex @@ -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 diff --git a/lib/federation/activity_pub/actor.ex b/lib/federation/activity_pub/actor.ex index 9cdf851e..7509ee7f 100644 --- a/lib/federation/activity_pub/actor.ex +++ b/lib/federation/activity_pub/actor.ex @@ -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} -> diff --git a/lib/federation/activity_pub/federator.ex b/lib/federation/activity_pub/federator.ex index 4bed7464..8f811f42 100644 --- a/lib/federation/activity_pub/federator.ex +++ b/lib/federation/activity_pub/federator.ex @@ -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") diff --git a/lib/federation/activity_pub/fetcher.ex b/lib/federation/activity_pub/fetcher.ex index 7a9f4a61..a64c813b 100644 --- a/lib/federation/activity_pub/fetcher.ex +++ b/lib/federation/activity_pub/fetcher.ex @@ -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 diff --git a/lib/federation/activity_pub/refresher.ex b/lib/federation/activity_pub/refresher.ex index 9f3f0a3b..ae60edff 100644 --- a/lib/federation/activity_pub/refresher.ex +++ b/lib/federation/activity_pub/refresher.ex @@ -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 diff --git a/lib/federation/activity_pub/relay.ex b/lib/federation/activity_pub/relay.ex index 37f4811f..9ea39354 100644 --- a/lib/federation/activity_pub/relay.ex +++ b/lib/federation/activity_pub/relay.ex @@ -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 diff --git a/lib/federation/activity_pub/transmogrifier.ex b/lib/federation/activity_pub/transmogrifier.ex index ba7fa647..6e330132 100644 --- a/lib/federation/activity_pub/transmogrifier.ex +++ b/lib/federation/activity_pub/transmogrifier.ex @@ -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} diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex index 0dd34cb6..c7b48c90 100644 --- a/lib/federation/activity_pub/types/actors.ex +++ b/lib/federation/activity_pub/types/actors.ex @@ -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, diff --git a/lib/federation/activity_pub/types/comments.ex b/lib/federation/activity_pub/types/comments.ex index 578ba6ed..9aabfa58 100644 --- a/lib/federation/activity_pub/types/comments.ex +++ b/lib/federation/activity_pub/types/comments.ex @@ -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 diff --git a/lib/federation/activity_pub/types/entity.ex b/lib/federation/activity_pub/types/entity.ex index 30335e76..0c4e233b 100644 --- a/lib/federation/activity_pub/types/entity.ex +++ b/lib/federation/activity_pub/types/entity.ex @@ -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 diff --git a/lib/federation/activity_pub/types/events.ex b/lib/federation/activity_pub/types/events.ex index 3ec86234..04812424 100644 --- a/lib/federation/activity_pub/types/events.ex +++ b/lib/federation/activity_pub/types/events.ex @@ -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 diff --git a/lib/federation/activity_pub/types/members.ex b/lib/federation/activity_pub/types/members.ex index a0e5f1e5..39c75ac8 100644 --- a/lib/federation/activity_pub/types/members.ex +++ b/lib/federation/activity_pub/types/members.ex @@ -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 diff --git a/lib/federation/activity_pub/utils.ex b/lib/federation/activity_pub/utils.ex index 7282be86..0cfab57e 100644 --- a/lib/federation/activity_pub/utils.ex +++ b/lib/federation/activity_pub/utils.ex @@ -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)) diff --git a/lib/federation/activity_stream/converter/comment.ex b/lib/federation/activity_stream/converter/comment.ex index f653009e..eaa15ac0 100644 --- a/lib/federation/activity_stream/converter/comment.ex +++ b/lib/federation/activity_stream/converter/comment.ex @@ -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 diff --git a/lib/federation/activity_stream/converter/event.ex b/lib/federation/activity_stream/converter/event.ex index b7d2c187..79b58551 100644 --- a/lib/federation/activity_stream/converter/event.ex +++ b/lib/federation/activity_stream/converter/event.ex @@ -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 diff --git a/lib/federation/activity_stream/converter/media.ex b/lib/federation/activity_stream/converter/media.ex index d89aa2de..6b63caab 100644 --- a/lib/federation/activity_stream/converter/media.ex +++ b/lib/federation/activity_stream/converter/media.ex @@ -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 diff --git a/lib/federation/activity_stream/converter/todo.ex b/lib/federation/activity_stream/converter/todo.ex index 0ac0eebb..09622a43 100644 --- a/lib/federation/activity_stream/converter/todo.ex +++ b/lib/federation/activity_stream/converter/todo.ex @@ -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 diff --git a/lib/federation/activity_stream/converter/utils.ex b/lib/federation/activity_stream/converter/utils.ex index a78456bd..a01be5bd 100644 --- a/lib/federation/activity_stream/converter/utils.ex +++ b/lib/federation/activity_stream/converter/utils.ex @@ -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 diff --git a/lib/federation/http_signatures/signature.ex b/lib/federation/http_signatures/signature.ex index 07ec0a82..2d7003eb 100644 --- a/lib/federation/http_signatures/signature.ex +++ b/lib/federation/http_signatures/signature.ex @@ -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") diff --git a/lib/federation/web_finger/web_finger.ex b/lib/federation/web_finger/web_finger.ex index 9066d135..65790eb2 100644 --- a/lib/federation/web_finger/web_finger.ex +++ b/lib/federation/web_finger/web_finger.ex @@ -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 diff --git a/lib/federation/web_finger/xml_builder.ex b/lib/federation/web_finger/xml_builder.ex index 76cc2cbe..a45df0dd 100644 --- a/lib/federation/web_finger/xml_builder.ex +++ b/lib/federation/web_finger/xml_builder.ex @@ -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() <> 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() <> 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 diff --git a/lib/graphql/api/events.ex b/lib/graphql/api/events.ex index 5e7e5bf2..6de8f12d 100644 --- a/lib/graphql/api/events.ex +++ b/lib/graphql/api/events.ex @@ -15,15 +15,8 @@ defmodule Mobilizon.GraphQL.API.Events do """ @spec create_event(map) :: {:ok, Activity.t(), Event.t()} | any def create_event(args) do - with organizer_actor <- Map.get(args, :organizer_actor), - args <- extract_pictures_from_event_body(args, organizer_actor), - args <- - Map.update(args, :picture, nil, fn picture -> - process_picture(picture, organizer_actor) - end) do - # For now we don't federate drafts but it will be needed if we want to edit them as groups - ActivityPub.create(:event, args, should_federate(args)) - end + # For now we don't federate drafts but it will be needed if we want to edit them as groups + ActivityPub.create(:event, prepare_args(args), should_federate(args)) end @doc """ @@ -31,23 +24,28 @@ defmodule Mobilizon.GraphQL.API.Events do """ @spec update_event(map, Event.t()) :: {:ok, Activity.t(), Event.t()} | any def update_event(args, %Event{} = event) do - with organizer_actor <- Map.get(args, :organizer_actor), - args <- extract_pictures_from_event_body(args, organizer_actor), - args <- - Map.update(args, :picture, nil, fn picture -> - process_picture(picture, organizer_actor) - end) do - ActivityPub.update(event, args, should_federate(args)) - end + ActivityPub.update(event, prepare_args(args), should_federate(args)) end @doc """ Trigger the deletion of an event """ + @spec delete_event(Event.t(), Actor.t(), boolean()) :: {:ok, Activity.t(), Entity.t()} | any() def delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) do ActivityPub.delete(event, actor, federate) end + @spec prepare_args(map) :: map + defp prepare_args(args) do + organizer_actor = Map.get(args, :organizer_actor) + + args + |> extract_pictures_from_event_body(organizer_actor) + |> Map.update(:picture, nil, fn picture -> + process_picture(picture, organizer_actor) + end) + end + defp process_picture(nil, _), do: nil defp process_picture(%{media_id: _picture_id} = args, _), do: args @@ -75,6 +73,7 @@ defmodule Mobilizon.GraphQL.API.Events do defp extract_pictures_from_event_body(args, _), do: args + @spec should_federate(map()) :: boolean defp should_federate(%{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id), do: true diff --git a/lib/graphql/api/follows.ex b/lib/graphql/api/follows.ex index b47cb75f..48925ead 100644 --- a/lib/graphql/api/follows.ex +++ b/lib/graphql/api/follows.ex @@ -7,72 +7,80 @@ defmodule Mobilizon.GraphQL.API.Follows do alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Federation.ActivityPub - alias Mobilizon.Federation.ActivityPub.Activity require Logger + @doc """ + Make an actor (`follower`) follow another (`followed`). + """ + @spec follow(follower :: Actor.t(), followed :: Actor.t()) :: + {:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()} + | {:error, String.t()} def follow(%Actor{} = follower, %Actor{} = followed) do - case ActivityPub.follow(follower, followed) do - {:ok, activity, follow} -> - {:ok, activity, follow} - - {:error, e} -> - Logger.warn("Error while following actor: #{inspect(e)}") - {:error, e} - - e -> - Logger.warn("Error while following actor: #{inspect(e)}") - {:error, e} - end + ActivityPub.follow(follower, followed) end + @doc """ + Make an actor (`follower`) unfollow another (`followed`). + """ + @spec unfollow(follower :: Actor.t(), followed :: Actor.t()) :: + {:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()} + | {:error, String.t()} def unfollow(%Actor{} = follower, %Actor{} = followed) do - case ActivityPub.unfollow(follower, followed) do - {:ok, activity, follow} -> - {:ok, activity, follow} - - e -> - Logger.warn("Error while unfollowing actor: #{inspect(e)}") - {:error, e} - end + ActivityPub.unfollow(follower, followed) end - def accept(%Actor{} = follower, %Actor{} = followed) do - Logger.debug("We're trying to accept a follow") + @doc """ + Make an actor (`followed`) accept the follow from another (`follower`). + """ + @spec accept(follower :: Actor.t(), followed :: Actor.t()) :: + {:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()} + | {:error, String.t()} + def accept(%Actor{url: follower_url} = follower, %Actor{url: followed_url} = followed) do + Logger.debug( + "We're trying to accept a follow: #{followed_url} is accepting #{follower_url} follow request." + ) + + case Actors.is_following(follower, followed) do + %Follower{approved: false} = follow -> + ActivityPub.accept( + :follow, + follow, + true + ) - with %Follower{approved: false} = follow <- - Actors.is_following(follower, followed), - {:ok, %Activity{} = activity, %Follower{approved: true} = follow} <- - ActivityPub.accept( - :follow, - follow, - true - ) do - {:ok, activity, follow} - else %Follower{approved: true} -> {:error, "Follow already accepted"} + + nil -> + {:error, "Can't accept follow: #{follower_url} is not following #{followed_url}."} end end - def reject(%Actor{} = follower, %Actor{} = followed) do - Logger.debug("We're trying to reject a follow") + @doc """ + Make an actor (`followed`) reject the follow from another (`follower`). + """ + @spec reject(follower :: Actor.t(), followed :: Actor.t()) :: + {:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()} + | {:error, String.t()} + def reject(%Actor{url: follower_url} = follower, %Actor{url: followed_url} = followed) do + Logger.debug( + "We're trying to reject a follow: #{followed_url} is rejecting #{follower_url} follow request." + ) - with {:follower, %Follower{} = follow} <- - {:follower, Actors.is_following(follower, followed)}, - {:ok, %Activity{} = activity, %Follower{} = follow} <- - ActivityPub.reject( - :follow, - follow, - true - ) do - {:ok, activity, follow} - else - {:follower, nil} -> - {:error, "Follow not found"} - - {:follower, %Follower{approved: true}} -> + case Actors.is_following(follower, followed) do + %Follower{approved: true} -> {:error, "Follow already accepted"} + + %Follower{} = follow -> + ActivityPub.reject( + :follow, + follow, + true + ) + + nil -> + {:error, "Follow not found"} end end end diff --git a/lib/graphql/api/participations.ex b/lib/graphql/api/participations.ex index 55ce95c1..ae1f7237 100644 --- a/lib/graphql/api/participations.ex +++ b/lib/graphql/api/participations.ex @@ -11,23 +11,23 @@ defmodule Mobilizon.GraphQL.API.Participations do alias Mobilizon.Service.Notifications.Scheduler alias Mobilizon.Web.Email.Participation - @spec join(Event.t(), Actor.t(), map()) :: {:ok, Activity.t(), Participant.t()} + @spec join(Event.t(), Actor.t(), map()) :: + {:ok, Activity.t(), Participant.t()} | {:error, :already_participant} def join(%Event{id: event_id} = event, %Actor{id: actor_id} = actor, args \\ %{}) do - with {:error, :participant_not_found} <- - Mobilizon.Events.get_participant(event_id, actor_id, args), - {:ok, activity, participant} <- - ActivityPub.join(event, actor, Map.get(args, :local, true), %{metadata: args}) do - {:ok, activity, participant} + case Mobilizon.Events.get_participant(event_id, actor_id, args) do + {:ok, %Participant{}} -> + {:error, :already_participant} + + {:error, :participant_not_found} -> + ActivityPub.join(event, actor, Map.get(args, :local, true), %{metadata: args}) end end - @spec leave(Event.t(), Actor.t()) :: {:ok, Activity.t(), Participant.t()} - def leave(%Event{} = event, %Actor{} = actor, args \\ %{}) do - with {:ok, activity, participant} <- - ActivityPub.leave(event, actor, Map.get(args, :local, true), %{metadata: args}) do - {:ok, activity, participant} - end - end + @spec leave(Event.t(), Actor.t(), map()) :: + {:ok, Activity.t(), Participant.t()} + | {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()} + def leave(%Event{} = event, %Actor{} = actor, args \\ %{}), + do: ActivityPub.leave(event, actor, Map.get(args, :local, true), %{metadata: args}) @doc """ Update participation status @@ -36,7 +36,6 @@ defmodule Mobilizon.GraphQL.API.Participations do def update(%Participant{} = participation, %Actor{} = moderator, :participant), do: accept(participation, moderator) - @spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()} def update(%Participant{} = participation, %Actor{} = _moderator, :not_approved) do with {:ok, %Participant{} = participant} <- Events.update_participant(participation, %{role: :not_approved}) do @@ -45,7 +44,6 @@ defmodule Mobilizon.GraphQL.API.Participations do end end - @spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()} def update(%Participant{} = participation, %Actor{} = moderator, :rejected), do: reject(participation, moderator) diff --git a/lib/graphql/api/search.ex b/lib/graphql/api/search.ex index f5b34225..6252cb04 100644 --- a/lib/graphql/api/search.ex +++ b/lib/graphql/api/search.ex @@ -59,8 +59,8 @@ defmodule Mobilizon.GraphQL.API.Search do @doc """ Search events """ - @spec search_events(String.t(), integer | nil, integer | nil) :: - {:ok, Page.t()} | {:error, String.t()} + @spec search_events(map(), integer | nil, integer | nil) :: + {:ok, Page.t()} def search_events(%{term: term} = args, page \\ 1, limit \\ 10) do term = String.trim(term) @@ -78,6 +78,7 @@ defmodule Mobilizon.GraphQL.API.Search do end end + @spec interact(String.t()) :: {:ok, struct()} | {:error, :not_found} def interact(uri) do case ActivityPub.fetch_object_from_url(uri) do {:ok, object} -> diff --git a/lib/graphql/resolvers/activity.ex b/lib/graphql/resolvers/activity.ex index 7184fd57..ae683341 100644 --- a/lib/graphql/resolvers/activity.ex +++ b/lib/graphql/resolvers/activity.ex @@ -15,23 +15,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do def group_activity(%Actor{type: :Group, id: group_id}, %{page: page, limit: limit} = args, %{ context: %{current_user: %User{role: role} = user} }) do - with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, - {:member, true} <- {:member, Actors.is_member?(actor_id, group_id) or is_moderator(role)} do - %Page{total: total, elements: elements} = - Activities.list_group_activities_for_member( - group_id, - actor_id, - [type: Map.get(args, :type), author: Map.get(args, :author)], - page, - limit - ) + case Users.get_actor_for_user(user) do + %Actor{id: actor_id} = _actor -> + if Actors.is_member?(actor_id, group_id) or is_moderator(role) do + %Page{total: total, elements: elements} = + Activities.list_group_activities_for_member( + group_id, + actor_id, + [type: Map.get(args, :type), author: Map.get(args, :author)], + page, + limit + ) - elements = Enum.map(elements, &Utils.transform_activity/1) + elements = Enum.map(elements, &Utils.transform_activity/1) - {:ok, %Page{total: total, elements: elements}} - else - {:member, false} -> - {:error, :unauthorized} + {:ok, %Page{total: total, elements: elements}} + else + {:error, :unauthorized} + end + + nil -> + {:error, :user_actor_not_found} end end diff --git a/lib/graphql/resolvers/admin.ex b/lib/graphql/resolvers/admin.ex index 42d3cd06..691230e8 100644 --- a/lib/graphql/resolvers/admin.ex +++ b/lib/graphql/resolvers/admin.ex @@ -311,7 +311,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do {:ok, _activity, follow} -> {:ok, follow} - {:error, err} when is_binary(err) -> + {:error, err} -> {:error, err} end end @@ -322,7 +322,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do {:ok, _activity, follow} -> {:ok, follow} - {:error, {:error, err}} when is_binary(err) -> + {:error, err} -> {:error, err} end end @@ -337,10 +337,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do {:ok, _activity, follow} -> {:ok, follow} - {:error, {:error, err}} when is_binary(err) -> - {:error, err} - - {:error, err} when is_binary(err) -> + {:error, err} -> {:error, err} end end @@ -355,10 +352,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do {:ok, _activity, follow} -> {:ok, follow} - {:error, {:error, err}} when is_binary(err) -> - {:error, err} - - {:error, err} when is_binary(err) -> + {:error, err} -> {:error, err} end end @@ -385,16 +379,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do do: Map.put(args, :name, new_instance_name), else: args - with {:changes, true} <- {:changes, args != %{}}, - %Actor{} = instance_actor <- Relay.get_actor(), - {:ok, _activity, _actor} <- ActivityPub.update(instance_actor, args, true) do - :ok - else - {:changes, false} -> - :ok + if args != %{} do + %Actor{} = instance_actor = Relay.get_actor() - err -> - err + case ActivityPub.update(instance_actor, args, true) do + {:ok, _activity, _actor} -> + :ok + + {:error, _err} -> + {:error, :instance_actor_update_failure} + end + else + :ok end end end diff --git a/lib/graphql/resolvers/event.ex b/lib/graphql/resolvers/event.ex index 318bd75e..d38c0a69 100644 --- a/lib/graphql/resolvers/event.ex +++ b/lib/graphql/resolvers/event.ex @@ -293,8 +293,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do %{context: %{current_user: %User{} = user}} = _resolution ) do # See https://github.com/absinthe-graphql/absinthe/issues/490 - with args <- Map.put(args, :options, args[:options] || %{}), - {:ok, %Event{} = event} <- Events.get_event_with_preload(event_id), + args = Map.put(args, :options, args[:options] || %{}) + + with {:ok, %Event{} = event} <- Events.get_event_with_preload(event_id), %Actor{} = actor <- Users.get_actor_for_user(user), {:ok, args} <- verify_profile_change(args, event, user, actor), {:event_can_be_managed, true} <- @@ -319,7 +320,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do {:new_actor, _} -> {:error, dgettext("errors", "You can't attribute this event to this profile.")} - {:error, _, %Ecto.Changeset{} = error, _} -> + {:error, %Ecto.Changeset{} = error} -> {:error, error} end end diff --git a/lib/graphql/resolvers/media.ex b/lib/graphql/resolvers/media.ex index 20ab8e90..27f17f94 100644 --- a/lib/graphql/resolvers/media.ex +++ b/lib/graphql/resolvers/media.ex @@ -94,6 +94,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do else {:media, nil} -> {:error, :not_found} {:is_owned, _} -> {:error, :unauthorized} + {:error, :enofile} -> {:error, "File not found"} end end diff --git a/lib/graphql/resolvers/member.ex b/lib/graphql/resolvers/member.ex index c41e19f4..8a873862 100644 --- a/lib/graphql/resolvers/member.ex +++ b/lib/graphql/resolvers/member.ex @@ -145,10 +145,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do ActivityPub.update(member, %{role: role}, true, %{moderator: moderator}) do {:ok, member} else - {:has_rights_to_update_role, {:error, :member_not_found}} -> + {:error, :member_not_found} -> {:error, dgettext("errors", "You are not a moderator or admin for this group")} - {:is_only_admin, true} -> + {:error, :only_admin_left} -> {:error, dgettext( "errors", diff --git a/lib/graphql/resolvers/participant.ex b/lib/graphql/resolvers/participant.ex index 9c41217f..ef2f5527 100644 --- a/lib/graphql/resolvers/participant.ex +++ b/lib/graphql/resolvers/participant.ex @@ -117,7 +117,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do |> Map.put(:actor, actor) do {:ok, participant} else - {:maximum_attendee_capacity, _} -> + {:error, :maximum_attendee_capacity_reached} -> {:error, dgettext("errors", "The event has already reached its maximum capacity")} {:has_event, _} -> @@ -127,11 +127,33 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do {:error, :event_not_found} -> {:error, dgettext("errors", "Event id not found")} - {:ok, %Participant{}} -> + {:error, :already_participant} -> {:error, dgettext("errors", "You are already a participant of this event")} end end + @spec check_anonymous_participation(String.t(), String.t()) :: + {:ok, Event.t()} | {:error, String.t()} + defp check_anonymous_participation(actor_id, event_id) do + cond do + Config.anonymous_participation?() == false -> + {:error, dgettext("errors", "Anonymous participation is not enabled")} + + to_string(Config.anonymous_actor_id()) != actor_id -> + {:error, dgettext("errors", "The anonymous actor ID is invalid")} + + true -> + case Mobilizon.Events.get_event_with_preload(event_id) do + {:ok, %Event{} = event} -> + {:ok, event} + + {:error, :event_not_found} -> + {:error, + dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))} + end + end + end + @doc """ Leave an event for an anonymous actor """ @@ -141,33 +163,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do _resolution ) when not is_nil(token) do - with {:anonymous_participation_enabled, true} <- - {:anonymous_participation_enabled, Config.anonymous_participation?()}, - {:anonymous_actor_id, true} <- - {:anonymous_actor_id, to_string(Config.anonymous_actor_id()) == actor_id}, - {:has_event, {:ok, %Event{} = event}} <- - {:has_event, Mobilizon.Events.get_event_with_preload(event_id)}, - %Actor{} = actor <- Actors.get_actor_with_preload(actor_id), - {:ok, _activity, %Participant{id: participant_id} = _participant} <- - Participations.leave(event, actor, %{local: false, cancellation_token: token}) do - {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}, id: participant_id}} - else - {:has_event, _} -> - {:error, - dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))} + case check_anonymous_participation(actor_id, event_id) do + {:ok, %Event{} = event} -> + %Actor{} = actor = Actors.get_actor_with_preload!(actor_id) - {:is_owned, nil} -> - {:error, dgettext("errors", "Profile is not owned by authenticated user")} + case Participations.leave(event, actor, %{local: false, cancellation_token: token}) do + {:ok, _activity, %Participant{id: participant_id} = _participant} -> + {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}, id: participant_id}} - {:only_organizer, true} -> - {:error, - dgettext( - "errors", - "You can't leave event because you're the only event creator participant" - )} + {:error, :is_only_organizer} -> + {:error, + dgettext( + "errors", + "You can't leave event because you're the only event creator participant" + )} - {:error, :participant_not_found} -> - {:error, dgettext("errors", "Participant not found")} + {:error, :participant_not_found} -> + {:error, dgettext("errors", "Participant not found")} + + {:error, _err} -> + {:error, dgettext("errors", "Failed to leave the event")} + end end end @@ -188,7 +204,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do {:is_owned, nil} -> {:error, dgettext("errors", "Profile is not owned by authenticated user")} - {:only_organizer, true} -> + {:error, :is_only_organizer} -> {:error, dgettext( "errors", diff --git a/lib/graphql/resolvers/user.ex b/lib/graphql/resolvers/user.ex index a141b014..803f5138 100644 --- a/lib/graphql/resolvers/user.ex +++ b/lib/graphql/resolvers/user.ex @@ -31,7 +31,10 @@ defmodule Mobilizon.GraphQL.Resolvers.User do @doc """ Return current logged-in user """ - def get_current_user(_parent, _args, %{context: %{current_user: %User{} = user}}) do + @spec get_current_user(any, map(), Absinthe.Resolution.t()) :: + {:error, :unauthenticated} | {:ok, Mobilizon.Users.User.t()} + def get_current_user(_parent, _args, %{context: %{current_user: %User{} = user} = context}) do + # Logger.error(inspect(context)) {:ok, user} end @@ -199,7 +202,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do @doc """ Validate an user, get its actor and a token """ - @spec validate_user(map(), %{token: String.t()}, map()) :: {:ok, map()} + @spec validate_user(map(), %{token: String.t()}, map()) :: {:ok, map()} | {:error, String.t()} def validate_user(_parent, %{token: token}, _resolution) do with {:check_confirmation_token, {:ok, %User{} = user}} <- {:check_confirmation_token, Email.User.check_confirmation_token(token)}, diff --git a/lib/mix/tasks/mobilizon/actors/new.ex b/lib/mix/tasks/mobilizon/actors/new.ex index 48124d81..b79a0ef0 100644 --- a/lib/mix/tasks/mobilizon/actors/new.ex +++ b/lib/mix/tasks/mobilizon/actors/new.ex @@ -94,7 +94,7 @@ defmodule Mix.Tasks.Mobilizon.Actors.New do {:admin, nil} -> shell_error("Profile with username #{Keyword.get(options, :group_admin)} wasn't found") - {:error, :insert_group, %Ecto.Changeset{errors: errors}, _} -> + {:error, %Ecto.Changeset{errors: errors}} -> shell_error(inspect(errors)) shell_error("Error while creating group because of the above reason") end diff --git a/lib/mix/tasks/mobilizon/actors/utils.ex b/lib/mix/tasks/mobilizon/actors/utils.ex index 38e09e17..28daefdf 100644 --- a/lib/mix/tasks/mobilizon/actors/utils.ex +++ b/lib/mix/tasks/mobilizon/actors/utils.ex @@ -50,6 +50,8 @@ defmodule Mix.Tasks.Mobilizon.Actors.Utils do new_person end + @spec create_group(Actor.t(), String.t(), String.t(), Keyword.t()) :: + {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} def create_group(%Actor{id: admin_id}, username, name, _options \\ []) do {username, name} = username_and_name(username, name) diff --git a/lib/mix/tasks/mobilizon/ecto.ex b/lib/mix/tasks/mobilizon/ecto.ex index 5ce88e5e..a2079e45 100644 --- a/lib/mix/tasks/mobilizon/ecto.ex +++ b/lib/mix/tasks/mobilizon/ecto.ex @@ -10,7 +10,7 @@ defmodule Mix.Tasks.Mobilizon.Ecto do @doc """ Ensures the given repository's migrations path exists on the file system. """ - @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t() + @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t() | no_return def ensure_migrations_path(repo, opts) do path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations") @@ -39,6 +39,7 @@ defmodule Mix.Tasks.Mobilizon.Ecto do Path.join(Application.app_dir(:mobilizon), priv) end + @spec raise_missing_migrations(String.t(), Ecto.Repo.t()) :: no_return defp raise_missing_migrations(path, repo) do raise(""" Could not find migrations directory #{inspect(path)} diff --git a/lib/mix/tasks/mobilizon/users/clean.ex b/lib/mix/tasks/mobilizon/users/clean.ex index 04f6f22d..336af5ab 100644 --- a/lib/mix/tasks/mobilizon/users/clean.ex +++ b/lib/mix/tasks/mobilizon/users/clean.ex @@ -71,7 +71,7 @@ defmodule Mix.Tasks.Mobilizon.Users.Clean do end) end - @spec result(boolean(), boolean()) :: :ok + @spec result(boolean(), non_neg_integer()) :: :ok defp result(dry_run, nb_deleted_users) do if dry_run do shell_info("#{nb_deleted_users} users would have been deleted") diff --git a/lib/mobilizon/activities/activity.ex b/lib/mobilizon/activities/activity.ex index 08698d2e..6e1681e2 100644 --- a/lib/mobilizon/activities/activity.ex +++ b/lib/mobilizon/activities/activity.ex @@ -15,7 +15,8 @@ defmodule Mobilizon.Activities.Activity do :message, :message_params, :object_type, - :object_id + :object_id, + :object ] @attrs @required_attrs ++ @optional_attrs @@ -28,6 +29,7 @@ defmodule Mobilizon.Activities.Activity do message_params: map(), object_type: ObjectType.t(), object_id: String.t(), + object: map(), author: Actor.t(), group: Actor.t() } @@ -41,12 +43,14 @@ defmodule Mobilizon.Activities.Activity do field(:message_params, :map, default: %{}) field(:object_type, ObjectType) field(:object_id, :string) + field(:object, :map, virtual: true) field(:inserted_at, :utc_datetime) belongs_to(:author, Actor) belongs_to(:group, Actor) end @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(activity, attrs) do activity |> cast(attrs, @attrs) diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 8de075b6..f4db670d 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -11,7 +11,7 @@ defmodule Mobilizon.Actors.Actor do alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member} alias Mobilizon.Addresses.Address alias Mobilizon.Discussions.Comment - alias Mobilizon.Events.{Event, FeedToken} + alias Mobilizon.Events.{Event, FeedToken, Participant} alias Mobilizon.Medias.File alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Users.User @@ -33,8 +33,8 @@ defmodule Mobilizon.Actors.Actor do posts_url: String.t(), events_url: String.t(), type: ActorType.t(), - name: String.t(), - domain: String.t(), + name: String.t() | nil, + domain: String.t() | nil, summary: String.t(), preferred_username: String.t(), keys: String.t(), @@ -42,12 +42,13 @@ defmodule Mobilizon.Actors.Actor do openness: ActorOpenness.t(), visibility: ActorVisibility.t(), suspended: boolean, - avatar: File.t(), - banner: File.t(), + avatar: File.t() | nil, + banner: File.t() | nil, user: User.t(), followers: [Follower.t()], followings: [Follower.t()], organized_events: [Event.t()], + participations: [Participant.t()], comments: [Comment.t()], feed_tokens: [FeedToken.t()], created_reports: [Report.t()], @@ -184,6 +185,7 @@ defmodule Mobilizon.Actors.Actor do has_many(:created_reports, Report, foreign_key: :reporter_id) has_many(:subject_reports, Report, foreign_key: :reported_id) has_many(:report_notes, Note, foreign_key: :moderator_id) + has_many(:participations, Participant, foreign_key: :actor_id) has_many(:mentions, Mention) has_many(:shares, Share, foreign_key: :actor_id) has_many(:owner_shares, Share, foreign_key: :owner_actor_id) @@ -243,7 +245,7 @@ defmodule Mobilizon.Actors.Actor do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = actor, attrs) do actor |> cast(attrs, @attrs) @@ -278,7 +280,7 @@ defmodule Mobilizon.Actors.Actor do @doc """ Changeset for person registration. """ - @spec registration_changeset(t, map) :: Ecto.Changeset.t() + @spec registration_changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def registration_changeset(%__MODULE__{} = actor, attrs) do actor |> cast(attrs, @registration_attrs) @@ -293,19 +295,14 @@ defmodule Mobilizon.Actors.Actor do """ @spec remote_actor_creation_changeset(map) :: Ecto.Changeset.t() def remote_actor_creation_changeset(attrs) do - changeset = - %__MODULE__{} - |> cast(attrs, @remote_actor_creation_attrs) - |> validate_required(@remote_actor_creation_required_attrs) - |> common_changeset(attrs) - |> unique_username_validator() - |> validate_required(:domain) - |> validate_length(:summary, max: 5000) - |> validate_length(:preferred_username, max: 100) - - Logger.debug("Remote actor creation: #{inspect(changeset)}") - - changeset + %__MODULE__{} + |> cast(attrs, @remote_actor_creation_attrs) + |> validate_required(@remote_actor_creation_required_attrs) + |> common_changeset(attrs) + |> unique_username_validator() + |> validate_required(:domain) + |> validate_length(:summary, max: 5000) + |> validate_length(:preferred_username, max: 100) end @spec common_changeset(Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() @@ -323,7 +320,7 @@ defmodule Mobilizon.Actors.Actor do @doc """ Changeset for group creation """ - @spec group_creation_changeset(t, map) :: Ecto.Changeset.t() + @spec group_creation_changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def group_creation_changeset(actor, params) do actor |> cast(params, @group_creation_attrs) @@ -416,12 +413,8 @@ defmodule Mobilizon.Actors.Actor do @spec build_relay_creation_attrs :: Ecto.Changeset.t() def build_relay_creation_attrs do data = %{ - name: Config.get([:instance, :name], "Mobilizon"), - summary: - Config.get( - [:instance, :description], - "An internal service actor for this Mobilizon instance" - ), + name: Config.instance_name(), + summary: Config.instance_description(), keys: Crypto.generate_rsa_2048_private_key(), preferred_username: "relay", domain: nil, diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 7811b220..fb903c9e 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -75,13 +75,20 @@ defmodule Mobilizon.Actors do @doc """ Gets an actor with preloaded relations. """ - @spec get_actor_with_preload(integer | String.t()) :: Actor.t() | nil + @spec get_actor_with_preload(integer | String.t(), boolean) :: Actor.t() | nil def get_actor_with_preload(id, include_suspended \\ false) do id |> actor_with_preload_query(include_suspended) |> Repo.one() end + @spec get_actor_with_preload!(integer | String.t(), boolean) :: Actor.t() + def get_actor_with_preload!(id, include_suspended \\ false) do + id + |> actor_with_preload_query(include_suspended) + |> Repo.one!() + end + @doc """ Gets a local actor with preloaded relations. """ @@ -148,9 +155,7 @@ defmodule Mobilizon.Actors do """ @spec get_actor_by_name(String.t(), ActorType.t() | nil) :: Actor.t() | nil def get_actor_by_name(name, type \\ nil) do - query = from(a in Actor) - - query + Actor |> filter_by_type(type) |> filter_by_name(name |> String.trim() |> String.trim_leading("@") |> String.split("@")) |> Repo.one() @@ -161,9 +166,7 @@ defmodule Mobilizon.Actors do """ @spec get_local_actor_by_name(String.t()) :: Actor.t() | nil def get_local_actor_by_name(name) do - query = from(a in Actor) - - query + Actor |> filter_by_name([name]) |> Repo.one() end @@ -210,7 +213,8 @@ defmodule Mobilizon.Actors do @doc """ Creates a new person actor. """ - @spec new_person(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} + @spec new_person(map, default_actor :: boolean()) :: + {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} def new_person(args, default_actor \\ false) do args = Map.put(args, :keys, Crypto.generate_rsa_2048_private_key()) @@ -323,8 +327,8 @@ defmodule Mobilizon.Actors do String.t(), boolean, boolean, - integer, - integer + integer | nil, + integer | nil ) :: Page.t() def list_actors( type \\ :Person, @@ -367,7 +371,23 @@ defmodule Mobilizon.Actors do |> Page.build_page(page, limit) end - @spec filter_actors(Ecto.Query.t(), String.t(), String.t(), String.t(), boolean(), boolean()) :: + @spec list_suspended_actors_to_purge(Keyword.t()) :: list(Actors.t()) + def list_suspended_actors_to_purge(options) do + suspension_days = Keyword.get(options, :suspension, 30) + + Actor + |> filter_suspended_days(suspension_days) + |> Repo.all() + end + + @spec filter_actors( + Ecto.Queryable.t(), + String.t(), + String.t(), + String.t(), + boolean(), + boolean() + ) :: Ecto.Query.t() defp filter_actors( query, @@ -403,14 +423,27 @@ defmodule Mobilizon.Actors do defp filter_remote(query, true), do: filter_local(query) defp filter_remote(query, false), do: filter_external(query) - @spec filter_suspended(Ecto.Query.t(), boolean()) :: Ecto.Query.t() + @spec filter_suspended(Ecto.Queryable.t(), boolean()) :: Ecto.Query.t() defp filter_suspended(query, true), do: where(query, [a], a.suspended) defp filter_suspended(query, false), do: where(query, [a], not a.suspended) - @spec filter_out_anonymous_actor_id(Ecto.Query.t(), integer() | String.t()) :: Ecto.Query.t() + @spec filter_out_anonymous_actor_id(Ecto.Queryable.t(), integer() | String.t()) :: + Ecto.Query.t() defp filter_out_anonymous_actor_id(query, anonymous_actor_id), do: where(query, [a], a.id != ^anonymous_actor_id) + @spec filter_suspended_days(Ecto.Queryable.t(), integer()) :: Ecto.Query.t() + defp filter_suspended_days(query, suspended_days) do + expiration_date = DateTime.add(DateTime.utc_now(), suspended_days * 24 * -3600) + + where( + query, + [a], + a.suspended and + a.updated_at > ^expiration_date + ) + end + @doc """ Returns the list of local actors by their username. """ @@ -486,14 +519,14 @@ defmodule Mobilizon.Actors do end end - @spec get_local_group_by_url(String.t()) :: Actor.t() + @spec get_local_group_by_url(String.t()) :: Actor.t() | nil def get_local_group_by_url(group_url) do group_query() |> where([q], q.url == ^group_url and is_nil(q.domain)) |> Repo.one() end - @spec get_group_by_members_url(String.t()) :: Actor.t() + @spec get_group_by_members_url(String.t()) :: Actor.t() | nil def get_group_by_members_url(members_url) do group_query() |> where([q], q.members_url == ^members_url) @@ -531,7 +564,7 @@ defmodule Mobilizon.Actors do {:ok, %{insert_group: %Actor{} = group, add_admin_member: %Member{} = _admin_member}} -> {:ok, group} - {:error, %Ecto.Changeset{} = err} -> + {:error, _err, %Ecto.Changeset{} = err, _} -> {:error, err} end else @@ -606,7 +639,7 @@ defmodule Mobilizon.Actors do @doc """ Gets a single member. """ - @spec get_member(integer | String.t()) :: {:ok, Member.t()} | nil + @spec get_member(integer | String.t()) :: Member.t() | nil def get_member(id) do Member |> Repo.get(id) @@ -623,7 +656,7 @@ defmodule Mobilizon.Actors do @doc """ Gets a single member of an actor (for example a group). """ - @spec get_member(integer | String.t(), integer | String.t()) :: + @spec get_member(actor_id :: integer | String.t(), parent_id :: integer | String.t()) :: {:ok, Member.t()} | {:error, :member_not_found} def get_member(actor_id, parent_id) do case Repo.get_by(Member, actor_id: actor_id, parent_id: parent_id) do @@ -1190,31 +1223,35 @@ defmodule Mobilizon.Actors do @doc """ Makes an actor following another actor. """ - @spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean | nil) :: - {:ok, Follower.t()} | {:error, atom, String.t()} + @spec follow( + followed :: Actor.t(), + follower :: Actor.t(), + url :: String.t() | nil, + approved :: boolean | nil + ) :: + {:ok, Follower.t()} + | {:error, :already_following | :followed_suspended | Ecto.Changeset.t()} def follow(%Actor{} = followed, %Actor{} = follower, url \\ nil, approved \\ true) do - with {:suspended, false} <- {:suspended, followed.suspended}, - # Check if followed has blocked follower - {:already_following, nil} <- {:already_following, is_following(follower, followed)} do - Logger.info( - "Making #{Actor.preferred_username_and_domain(follower)} follow #{Actor.preferred_username_and_domain(followed)} " <> - "(approved: #{approved})" - ) - - create_follower(%{ - "actor_id" => follower.id, - "target_actor_id" => followed.id, - "approved" => approved, - "url" => url - }) + if followed.suspended do + {:error, :followed_suspended} else - {:already_following, %Follower{}} -> - {:error, :already_following, - "Could not follow actor: you are already following #{Actor.preferred_username_and_domain(followed)}"} + case is_following(follower, followed) do + %Follower{} -> + {:error, :already_following} - {:suspended, _} -> - {:error, :suspended, - "Could not follow actor: #{Actor.preferred_username_and_domain(followed)} has been suspended"} + nil -> + Logger.info( + "Making #{Actor.preferred_username_and_domain(follower)} follow #{Actor.preferred_username_and_domain(followed)} " <> + "(approved: #{approved})" + ) + + create_follower(%{ + "actor_id" => follower.id, + "target_actor_id" => followed.id, + "approved" => approved, + "url" => url + }) + end end end @@ -1331,7 +1368,7 @@ defmodule Mobilizon.Actors do ) end - @spec actor_by_username_or_name_query(Ecto.Query.t(), String.t()) :: Ecto.Query.t() + @spec actor_by_username_or_name_query(Ecto.Queryable.t(), String.t()) :: Ecto.Query.t() defp actor_by_username_or_name_query(query, ""), do: query defp actor_by_username_or_name_query(query, username) do @@ -1358,7 +1395,7 @@ defmodule Mobilizon.Actors do ) end - @spec actors_for_location(Ecto.Query.t(), String.t(), integer()) :: Ecto.Query.t() + @spec actors_for_location(Ecto.Queryable.t(), String.t(), integer()) :: Ecto.Query.t() defp actors_for_location(query, location, radius) when is_valid_string(location) and not is_nil(radius) do with {lon, lat} <- Geohax.decode(location), @@ -1474,7 +1511,7 @@ defmodule Mobilizon.Actors do |> select([m, _a], m) end - @spec filter_member_role(Ecto.Query.t(), list(atom()) | atom()) :: Ecto.Query.t() + @spec filter_member_role(Ecto.Queryable.t(), list(atom()) | atom()) :: Ecto.Query.t() defp filter_member_role(query, []), do: query defp filter_member_role(query, roles) when is_list(roles) do @@ -1597,24 +1634,24 @@ defmodule Mobilizon.Actors do |> order_by(desc: :updated_at) end - @spec filter_local(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_local(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_local(query) do from(a in query, where: is_nil(a.domain)) end - @spec filter_external(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_external(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_external(query) do from(a in query, where: not is_nil(a.domain)) end - @spec filter_follower_actors_external(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_follower_actors_external(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_follower_actors_external(query) do query |> where([_f, a], not is_nil(a.domain)) |> preload([f, a], [:target_actor, :actor]) end - @spec filter_by_type(Ecto.Query.t(), ActorType.t()) :: Ecto.Query.t() + @spec filter_by_type(Ecto.Queryable.t(), ActorType.t() | nil) :: Ecto.Queryable.t() defp filter_by_type(query, type) when type in [:Person, :Group, :Application, :Service, :Organisation] do from(a in query, where: a.type == ^type) @@ -1622,12 +1659,12 @@ defmodule Mobilizon.Actors do defp filter_by_type(query, _type), do: query - @spec filter_by_types(Ecto.Query.t(), [ActorType.t()]) :: Ecto.Query.t() + @spec filter_by_types(Ecto.Queryable.t(), [ActorType.t()]) :: Ecto.Query.t() defp filter_by_types(query, types) do from(a in query, where: a.type in ^types) end - @spec filter_by_minimum_visibility(Ecto.Query.t(), atom()) :: Ecto.Query.t() + @spec filter_by_minimum_visibility(Ecto.Queryable.t(), atom()) :: Ecto.Query.t() defp filter_by_minimum_visibility(query, :private), do: query defp filter_by_minimum_visibility(query, :restricted) do @@ -1642,7 +1679,7 @@ defmodule Mobilizon.Actors do from(a in query, where: a.visibility == ^:public) end - @spec filter_by_name(query :: Ecto.Query.t(), [String.t()]) :: Ecto.Query.t() + @spec filter_by_name(query :: Ecto.Queryable.t(), [String.t()]) :: Ecto.Query.t() defp filter_by_name(query, [name]) do where(query, [a], a.preferred_username == ^name and is_nil(a.domain)) end @@ -1655,7 +1692,7 @@ defmodule Mobilizon.Actors do end end - @spec filter_followed_by_approved_status(Ecto.Query.t(), boolean() | nil) :: Ecto.Query.t() + @spec filter_followed_by_approved_status(Ecto.Queryable.t(), boolean() | nil) :: Ecto.Query.t() defp filter_followed_by_approved_status(query, nil), do: query defp filter_followed_by_approved_status(query, approved) do diff --git a/lib/mobilizon/actors/bot.ex b/lib/mobilizon/actors/bot.ex index 7946ec14..6c3af7f4 100644 --- a/lib/mobilizon/actors/bot.ex +++ b/lib/mobilizon/actors/bot.ex @@ -32,7 +32,7 @@ defmodule Mobilizon.Actors.Bot do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = bot, attrs) do bot |> cast(attrs, @attrs) diff --git a/lib/mobilizon/actors/follower.ex b/lib/mobilizon/actors/follower.ex index 7bc0ad92..396fb4b6 100644 --- a/lib/mobilizon/actors/follower.ex +++ b/lib/mobilizon/actors/follower.ex @@ -38,7 +38,7 @@ defmodule Mobilizon.Actors.Follower do end @doc false - @spec changeset(follower :: t, attrs :: map) :: Ecto.Changeset.t() + @spec changeset(follower :: t | Ecto.Schema.t(), attrs :: map) :: Ecto.Changeset.t() def changeset(follower, attrs) do follower |> cast(attrs, @attrs) diff --git a/lib/mobilizon/actors/member.ex b/lib/mobilizon/actors/member.ex index e2481b0f..a79ab2c5 100644 --- a/lib/mobilizon/actors/member.ex +++ b/lib/mobilizon/actors/member.ex @@ -59,7 +59,7 @@ defmodule Mobilizon.Actors.Member do def is_administrator(%__MODULE__{}), do: false @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = member, attrs) do member |> cast(attrs, @attrs) diff --git a/lib/mobilizon/addresses/address.ex b/lib/mobilizon/addresses/address.ex index d1de602b..7ccad919 100644 --- a/lib/mobilizon/addresses/address.ex +++ b/lib/mobilizon/addresses/address.ex @@ -57,7 +57,7 @@ defmodule Mobilizon.Addresses.Address do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = address, attrs) do address |> cast(attrs, @attrs) diff --git a/lib/mobilizon/addresses/addresses.ex b/lib/mobilizon/addresses/addresses.ex index 68137e00..6d1440eb 100644 --- a/lib/mobilizon/addresses/addresses.ex +++ b/lib/mobilizon/addresses/addresses.ex @@ -118,7 +118,7 @@ defmodule Mobilizon.Addresses do ) end - @spec order_by_coords(Ecto.Query.t(), map | nil) :: Ecto.Query.t() + @spec order_by_coords(Ecto.Queryable.t(), map | nil) :: Ecto.Query.t() defp order_by_coords(query, nil), do: query defp order_by_coords(query, coords) do @@ -128,7 +128,7 @@ defmodule Mobilizon.Addresses do ) end - @spec filter_by_contry(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t() + @spec filter_by_contry(Ecto.Queryable.t(), String.t() | nil) :: Ecto.Query.t() defp filter_by_contry(query, nil), do: query defp filter_by_contry(query, country) do diff --git a/lib/mobilizon/admin/action_log.ex b/lib/mobilizon/admin/action_log.ex index 9aa98000..a40a8b70 100644 --- a/lib/mobilizon/admin/action_log.ex +++ b/lib/mobilizon/admin/action_log.ex @@ -35,7 +35,7 @@ defmodule Mobilizon.Admin.ActionLog do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = action_log, attrs) do action_log |> cast(attrs, @attrs) diff --git a/lib/mobilizon/admin/admin.ex b/lib/mobilizon/admin/admin.ex index 21cb7cf1..a1e68c61 100644 --- a/lib/mobilizon/admin/admin.ex +++ b/lib/mobilizon/admin/admin.ex @@ -45,7 +45,8 @@ defmodule Mobilizon.Admin do @doc """ Log an admin action """ - @spec log_action(Actor.t(), String.t(), String.t()) :: {:ok, ActionLog.t()} + @spec log_action(Actor.t(), String.t(), struct()) :: + {:ok, ActionLog.t()} | {:error, Ecto.Changeset.t() | :user_not_moderator} def log_action(%Actor{user_id: user_id, id: actor_id}, action, target) do with %User{role: role} <- Users.get_user!(user_id), {:role, true} <- {:role, role in [:administrator, :moderator]}, @@ -58,6 +59,9 @@ defmodule Mobilizon.Admin do "changes" => stringify_struct(target) }) do {:ok, create_action_log} + else + {:role, false} -> + {:error, :user_not_moderator} end end @@ -109,12 +113,7 @@ defmodule Mobilizon.Admin do end end - def set_admin_setting_value(group, name, value) do - Setting - |> Setting.changeset(%{group: group, name: name, value: value}) - |> Repo.insert(on_conflict: :replace_all, conflict_target: [:group, :name]) - end - + @spec save_settings(String.t(), map()) :: {:ok, any} | {:error, any} def save_settings(group, args) do Multi.new() |> do_save_setting(group, args) @@ -125,6 +124,7 @@ defmodule Mobilizon.Admin do Setting |> where([s], s.group == ^group) |> Repo.delete_all() end + @spec do_save_setting(Ecto.Multi.t(), String.t(), map()) :: Ecto.Multi.t() defp do_save_setting(transaction, _group, args) when args == %{}, do: transaction defp do_save_setting(transaction, group, args) do @@ -147,6 +147,7 @@ defmodule Mobilizon.Admin do do_save_setting(transaction, group, rest) end + @spec convert_to_string(any()) :: String.t() defp convert_to_string(val) do case val do val when is_list(val) -> Jason.encode!(val) diff --git a/lib/mobilizon/admin/setting.ex b/lib/mobilizon/admin/setting.ex index 894a4ad9..a505358e 100644 --- a/lib/mobilizon/admin/setting.ex +++ b/lib/mobilizon/admin/setting.ex @@ -9,6 +9,12 @@ defmodule Mobilizon.Admin.Setting do @optional_attrs [:value] @attrs @required_attrs ++ @optional_attrs + @type t :: %{ + group: String.t(), + name: String.t(), + value: String.t() + } + schema "admin_settings" do field(:group, :string) field(:name, :string) @@ -18,6 +24,7 @@ defmodule Mobilizon.Admin.Setting do end @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(setting, attrs) do setting |> cast(attrs, @attrs) diff --git a/lib/mobilizon/config.ex b/lib/mobilizon/config.ex index 704fa63a..9a26ae4d 100644 --- a/lib/mobilizon/config.ex +++ b/lib/mobilizon/config.ex @@ -7,7 +7,24 @@ defmodule Mobilizon.Config do alias Mobilizon.Service.GitStatus require Logger - @spec instance_config :: keyword + @type mobilizon_config :: [ + name: String.t(), + description: String.t(), + hostname: String.t(), + registrations_open: boolean(), + languages: list(String.t()), + default_language: String.t(), + registration_email_allowlist: list(String.t()), + registration_email_denylist: list(String.t()), + demo: boolean(), + repository: String.t(), + email_from: String.t(), + email_reply_to: String.t(), + federating: boolean(), + remove_orphan_uploads: boolean() + ] + + @spec instance_config :: mobilizon_config def instance_config, do: Application.get_env(:mobilizon, :instance) @spec instance_name :: String.t() @@ -139,10 +156,10 @@ defmodule Mobilizon.Config do def instance_user_agent, do: "#{instance_hostname()} - Mobilizon #{instance_version()}" - @spec instance_federating :: String.t() + @spec instance_federating :: boolean() def instance_federating, do: instance_config()[:federating] - @spec instance_geocoding_provider :: atom() + @spec instance_geocoding_provider :: module() def instance_geocoding_provider, do: get_in(Application.get_env(:mobilizon, Mobilizon.Service.Geospatial), [:service]) @@ -150,63 +167,90 @@ defmodule Mobilizon.Config do def instance_geocoding_autocomplete, do: instance_geocoding_provider() !== Mobilizon.Service.Geospatial.Nominatim + @spec maps_config :: [ + tiles: [endpoint: String.t(), attribution: String.t()], + rounting: [type: atom] + ] + defp maps_config, do: Application.get_env(:mobilizon, :maps) + @spec instance_maps_tiles_endpoint :: String.t() - def instance_maps_tiles_endpoint, do: Application.get_env(:mobilizon, :maps)[:tiles][:endpoint] + def instance_maps_tiles_endpoint, do: maps_config()[:tiles][:endpoint] @spec instance_maps_tiles_attribution :: String.t() def instance_maps_tiles_attribution, - do: Application.get_env(:mobilizon, :maps)[:tiles][:attribution] + do: maps_config()[:tiles][:attribution] @spec instance_maps_routing_type :: atom() def instance_maps_routing_type, - do: Application.get_env(:mobilizon, :maps)[:routing][:type] + do: maps_config()[:routing][:type] + + @typep anonymous_config_type :: [ + participation: [ + allowed: boolean, + validation: [ + email: [enabled: boolean(), confirmation_required: boolean()], + captcha: [enabled: boolean()] + ] + ], + event_creation: [ + allowed: boolean, + validation: [ + email: [enabled: boolean(), confirmation_required: boolean()], + captcha: [enabled: boolean()] + ] + ], + reports: [ + allowed: boolean() + ] + ] + + @spec anonymous_config :: anonymous_config_type + defp anonymous_config, do: Application.get_env(:mobilizon, :anonymous) @spec anonymous_participation? :: boolean def anonymous_participation?, - do: Application.get_env(:mobilizon, :anonymous)[:participation][:allowed] + do: anonymous_config()[:participation][:allowed] @spec anonymous_participation_email_required? :: boolean def anonymous_participation_email_required?, - do: Application.get_env(:mobilizon, :anonymous)[:participation][:validation][:email][:enabled] + do: anonymous_config()[:participation][:validation][:email][:enabled] @spec anonymous_participation_email_confirmation_required? :: boolean def anonymous_participation_email_confirmation_required?, do: - Application.get_env(:mobilizon, :anonymous)[:participation][:validation][:email][ + anonymous_config()[:participation][:validation][:email][ :confirmation_required ] @spec anonymous_participation_email_captcha_required? :: boolean def anonymous_participation_email_captcha_required?, - do: - Application.get_env(:mobilizon, :anonymous)[:participation][:validation][:captcha][:enabled] + do: anonymous_config()[:participation][:validation][:captcha][:enabled] @spec anonymous_event_creation? :: boolean def anonymous_event_creation?, - do: Application.get_env(:mobilizon, :anonymous)[:event_creation][:allowed] + do: anonymous_config()[:event_creation][:allowed] @spec anonymous_event_creation_email_required? :: boolean def anonymous_event_creation_email_required?, - do: - Application.get_env(:mobilizon, :anonymous)[:event_creation][:validation][:email][:enabled] + do: anonymous_config()[:event_creation][:validation][:email][:enabled] @spec anonymous_event_creation_email_confirmation_required? :: boolean def anonymous_event_creation_email_confirmation_required?, do: - Application.get_env(:mobilizon, :anonymous)[:event_creation][:validation][:email][ + anonymous_config()[:event_creation][:validation][:email][ :confirmation_required ] @spec anonymous_event_creation_email_captcha_required? :: boolean def anonymous_event_creation_email_captcha_required?, do: - Application.get_env(:mobilizon, :anonymous)[:event_creation][:validation][:captcha][ + anonymous_config()[:event_creation][:validation][:captcha][ :enabled ] @spec anonymous_reporting? :: boolean def anonymous_reporting?, - do: Application.get_env(:mobilizon, :anonymous)[:reports][:allowed] + do: anonymous_config()[:reports][:allowed] @spec oauth_consumer_strategies() :: list({atom(), String.t()}) def oauth_consumer_strategies do @@ -265,7 +309,7 @@ defmodule Mobilizon.Config do @spec admin_settings :: map def admin_settings, do: get_cached_value(:admin_config) - @spec get(key :: module | atom) :: any + @spec get(keys :: module | atom | [module | atom]) :: any def get(key), do: get(key, nil) @spec get(keys :: [module | atom], default :: any) :: any @@ -281,7 +325,7 @@ defmodule Mobilizon.Config do @spec get(key :: module | atom, default :: any) :: any def get(key, default), do: Application.get_env(:mobilizon, key, default) - @spec get!(key :: module | atom) :: any + @spec get!(key :: module | atom) :: any | no_return def get!(key) do value = get(key, nil) diff --git a/lib/mobilizon/discussions/comment.ex b/lib/mobilizon/discussions/comment.ex index 3141e4ae..eed8f7ac 100644 --- a/lib/mobilizon/discussions/comment.ex +++ b/lib/mobilizon/discussions/comment.ex @@ -87,7 +87,7 @@ defmodule Mobilizon.Discussions.Comment do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = comment, attrs) do comment |> common_changeset(attrs) diff --git a/lib/mobilizon/discussions/discussion.ex b/lib/mobilizon/discussions/discussion.ex index 99ac9379..c6c98fab 100644 --- a/lib/mobilizon/discussions/discussion.ex +++ b/lib/mobilizon/discussions/discussion.ex @@ -59,7 +59,7 @@ defmodule Mobilizon.Discussions.Discussion do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = discussion, attrs) do discussion |> cast(attrs, @attrs) diff --git a/lib/mobilizon/discussions/discussions.ex b/lib/mobilizon/discussions/discussions.ex index 77106c47..9dccca78 100644 --- a/lib/mobilizon/discussions/discussions.ex +++ b/lib/mobilizon/discussions/discussions.ex @@ -71,7 +71,7 @@ defmodule Mobilizon.Discussions do We only get first comment of thread, and count replies. Read: https://hexdocs.pm/absinthe/ecto.html#dataloader """ - @spec query(atom(), map()) :: Ecto.Queryable.t() + @spec query(atom(), map()) :: Ecto.Query.t() def query(Comment, %{top_level: true}) do Comment |> join(:left, [c], r in Comment, on: r.origin_comment_id == c.id) @@ -158,7 +158,7 @@ defmodule Mobilizon.Discussions do Gets a comment by its URL, with all associations loaded. Raises `Ecto.NoResultsError` if the comment does not exist. """ - @spec get_comment_from_url_with_preload(String.t()) :: Comment.t() + @spec get_comment_from_url_with_preload!(String.t()) :: Comment.t() def get_comment_from_url_with_preload!(url) do Comment |> Repo.get_by!(url: url) @@ -168,7 +168,7 @@ defmodule Mobilizon.Discussions do @doc """ Gets a comment by its UUID, with all associations loaded. """ - @spec get_comment_from_uuid_with_preload(String.t()) :: Comment.t() + @spec get_comment_from_uuid_with_preload(String.t()) :: Comment.t() | nil def get_comment_from_uuid_with_preload(uuid) do Comment |> Repo.get_by(uuid: uuid) @@ -355,7 +355,7 @@ defmodule Mobilizon.Discussions do @doc """ Get a discussion by it's slug """ - @spec get_discussion_by_slug(String.t()) :: Discussion.t() + @spec get_discussion_by_slug(String.t()) :: Discussion.t() | nil def get_discussion_by_slug(discussion_slug) do Discussion |> Repo.get_by(slug: discussion_slug) @@ -494,11 +494,11 @@ defmodule Mobilizon.Discussions do ) end - @spec filter_comments_under_events(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_comments_under_events(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_comments_under_events(query) do where(query, [c], is_nil(c.discussion_id) and not is_nil(c.event_id)) end - @spec preload_for_comment(Ecto.Query.t()) :: Ecto.Query.t() + @spec preload_for_comment(Ecto.Queryable.t()) :: Ecto.Query.t() defp preload_for_comment(query), do: preload(query, ^@comment_preloads) end diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index 52b0f5ce..27eee288 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -35,6 +35,7 @@ defmodule Mobilizon.Events.Event do alias Mobilizon.Web.Router.Helpers, as: Routes @type t :: %__MODULE__{ + id: String.t(), url: String.t(), local: boolean, begins_on: DateTime.t(), @@ -53,7 +54,7 @@ defmodule Mobilizon.Events.Event do category: String.t(), options: EventOptions.t(), organizer_actor: Actor.t(), - attributed_to: Actor.t(), + attributed_to: Actor.t() | nil, physical_address: Address.t(), picture: Media.t(), media: [Media.t()], @@ -130,7 +131,7 @@ defmodule Mobilizon.Events.Event do end @doc false - @spec changeset(t, map) :: Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Changeset.t() def changeset(%__MODULE__{} = event, attrs) do attrs = Map.update(attrs, :uuid, Ecto.UUID.generate(), & &1) attrs = Map.update(attrs, :url, Routes.page_url(Endpoint, :event, attrs.uuid), & &1) @@ -289,4 +290,12 @@ defmodule Mobilizon.Events.Event do defp put_creator_if_published(%Changeset{} = changeset, _), do: cast_embed(changeset, :participant_stats) + + @doc """ + Whether we can show the event. Returns false if the organizer actor or group is suspended + """ + @spec show?(t) :: boolean() + def show?(%__MODULE__{attributed_to: %Actor{suspended: true}}), do: false + def show?(%__MODULE__{organizer_actor: %Actor{suspended: true}}), do: false + def show?(%__MODULE__{}), do: true end diff --git a/lib/mobilizon/events/event_metadata.ex b/lib/mobilizon/events/event_metadata.ex index 6ef9add3..3e9115fe 100644 --- a/lib/mobilizon/events/event_metadata.ex +++ b/lib/mobilizon/events/event_metadata.ex @@ -36,7 +36,7 @@ defmodule Mobilizon.Events.EventMetadata do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = event_metadata, attrs) do event_metadata |> cast(attrs, @attrs) diff --git a/lib/mobilizon/events/event_options.ex b/lib/mobilizon/events/event_options.ex index 6f1f5832..30336975 100644 --- a/lib/mobilizon/events/event_options.ex +++ b/lib/mobilizon/events/event_options.ex @@ -64,7 +64,7 @@ defmodule Mobilizon.Events.EventOptions do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = event_options, attrs) do event_options |> cast(attrs, @attrs) diff --git a/lib/mobilizon/events/event_participant_stats.ex b/lib/mobilizon/events/event_participant_stats.ex index 5ba4c11f..ea59494e 100644 --- a/lib/mobilizon/events/event_participant_stats.ex +++ b/lib/mobilizon/events/event_participant_stats.ex @@ -40,7 +40,7 @@ defmodule Mobilizon.Events.EventParticipantStats do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = event_options, attrs) do event_options |> cast(attrs, @attrs) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 4fc85511..fdd1579b 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -263,7 +263,10 @@ defmodule Mobilizon.Events do @doc """ Creates an event. """ - @spec create_event(map) :: {:ok, Event.t()} | {:error, Changeset.t()} + @spec create_event(map) :: + {:ok, Event.t()} + | {:error, Changeset.t()} + | {:error, :update | :write, Changeset.t(), map()} def create_event(attrs \\ %{}) do with {:ok, %{insert: %Event{} = event}} <- do_create_event(attrs), %Event{} = event <- Repo.preload(event, @event_preloads) do @@ -278,7 +281,10 @@ defmodule Mobilizon.Events do end # We start by inserting the event and then insert a first participant if the event is not a draft - @spec do_create_event(map) :: {:ok, Event.t()} | {:error, Changeset.t()} + @spec do_create_event(map) :: + {:ok, Event.t()} + | {:error, Changeset.t()} + | {:error, :update | :write, Changeset.t(), map()} defp do_create_event(attrs) do Multi.new() |> Multi.insert(:insert, Event.changeset(%Event{}, attrs)) @@ -307,7 +313,10 @@ defmodule Mobilizon.Events do We start by updating the event and then insert a first participant if the event is not a draft anymore """ - @spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Changeset.t()} + @spec update_event(Event.t(), map) :: + {:ok, Event.t()} + | {:error, Changeset.t()} + | {:error, :update | :write, Changeset.t(), map()} def update_event(%Event{draft: old_draft} = old_event, attrs) do with %Event{} = old_event <- Repo.preload(old_event, @event_preloads), %Changeset{changes: changes} = changeset <- @@ -394,7 +403,7 @@ defmodule Mobilizon.Events do |> Repo.stream() end - @spec list_public_local_events(integer | nil, integer | nil) :: Page.t() + @spec list_public_local_events(integer | nil, integer | nil) :: Page.t(Event.t()) def list_public_local_events(page \\ nil, limit \\ nil) do Event |> filter_public_visibility() @@ -452,7 +461,7 @@ defmodule Mobilizon.Events do DateTime.t() | nil, integer | nil, integer | nil - ) :: Page.t() + ) :: Page.t(Event.t()) def list_organized_events_for_group( %Actor{id: group_id}, visibility \\ :public, @@ -729,7 +738,7 @@ defmodule Mobilizon.Events do nil """ - @spec get_participant(integer) :: Participant.t() + @spec get_participant(integer) :: Participant.t() | nil def get_participant(participant_id) do Participant |> where([p], p.id == ^participant_id) @@ -1040,21 +1049,18 @@ defmodule Mobilizon.Events do Deletes a participant. """ @spec delete_participant(Participant.t()) :: - {:ok, Participant.t()} | {:error, Changeset.t()} + {:ok, %{participant: Participant.t()}} + | {:error, :participant | :update_event_participation_stats, Changeset.t(), map()} def delete_participant(%Participant{role: old_role} = participant) do - with {:ok, %{participant: %Participant{} = participant}} <- - Multi.new() - |> Multi.delete(:participant, participant) - |> Multi.run(:update_event_participation_stats, fn _repo, - %{ - participant: - %Participant{} = participant - } -> - update_participant_stats(participant, old_role, nil) - end) - |> Repo.transaction() do - {:ok, participant} - end + Multi.new() + |> Multi.delete(:participant, participant) + |> Multi.run(:update_event_participation_stats, fn _repo, + %{ + participant: %Participant{} = participant + } -> + update_participant_stats(participant, old_role, nil) + end) + |> Repo.transaction() end defp update_participant_stats( @@ -1330,7 +1336,7 @@ defmodule Mobilizon.Events do ) end - @spec user_events_query(Ecto.Query.t(), number()) :: Ecto.Query.t() + @spec user_events_query(Ecto.Queryable.t(), number()) :: Ecto.Query.t() defp user_events_query(query, user_id) do from( e in query, @@ -1373,7 +1379,7 @@ defmodule Mobilizon.Events do ) end - @spec events_for_begins_on(Ecto.Query.t(), map()) :: Ecto.Query.t() + @spec events_for_begins_on(Ecto.Queryable.t(), map()) :: Ecto.Query.t() defp events_for_begins_on(query, args) do begins_on = Map.get(args, :begins_on, DateTime.utc_now()) @@ -1381,7 +1387,7 @@ defmodule Mobilizon.Events do |> where([q], q.begins_on >= ^begins_on) end - @spec events_for_ends_on(Ecto.Query.t(), map()) :: Ecto.Query.t() + @spec events_for_ends_on(Ecto.Queryable.t(), map()) :: Ecto.Query.t() defp events_for_ends_on(query, args) do ends_on = Map.get(args, :ends_on) @@ -1396,7 +1402,7 @@ defmodule Mobilizon.Events do ) end - @spec events_for_tags(Ecto.Query.t(), map()) :: Ecto.Query.t() + @spec events_for_tags(Ecto.Queryable.t(), map()) :: Ecto.Query.t() defp events_for_tags(query, %{tags: tags}) when is_valid_string(tags) do query |> join(:inner, [q], te in "events_tags", on: q.id == te.event_id) @@ -1406,7 +1412,7 @@ defmodule Mobilizon.Events do defp events_for_tags(query, _args), do: query - @spec events_for_location(Ecto.Query.t(), map()) :: Ecto.Query.t() + @spec events_for_location(Ecto.Queryable.t(), map()) :: Ecto.Query.t() defp events_for_location(query, %{radius: radius}) when is_nil(radius), do: query @@ -1472,7 +1478,7 @@ defmodule Mobilizon.Events do from(t in Tag, where: t.title == ^title, limit: 1) end - @spec tag_filter(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t() + @spec tag_filter(Ecto.Queryable.t(), String.t() | nil) :: Ecto.Query.t() defp tag_filter(query, nil), do: query defp tag_filter(query, ""), do: query @@ -1511,7 +1517,7 @@ defmodule Mobilizon.Events do ) end - @spec tag_relation_union_subquery(Ecto.Query.t(), integer) :: Ecto.Query.t() + @spec tag_relation_union_subquery(Ecto.Queryable.t(), integer) :: Ecto.Query.t() defp tag_relation_union_subquery(subquery, tag_id) do from( tr in TagRelation, @@ -1521,7 +1527,7 @@ defmodule Mobilizon.Events do ) end - @spec tag_neighbors_query(Ecto.Query.t(), integer, integer) :: Ecto.Query.t() + @spec tag_neighbors_query(Ecto.Queryable.t(), integer, integer) :: Ecto.Query.t() defp tag_neighbors_query(subquery, relation_minimum, limit) do from( t in Tag, @@ -1673,38 +1679,34 @@ defmodule Mobilizon.Events do from(tk in FeedToken, where: tk.actor_id == ^actor_id, preload: [:actor, :user]) end - @spec filter_public_visibility(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_public_visibility(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_public_visibility(query) do from(e in query, where: e.visibility == ^:public) end - @spec filter_unlisted_and_public_visibility(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_unlisted_and_public_visibility(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_unlisted_and_public_visibility(query) do from(q in query, where: q.visibility in ^@public_visibility) end - @spec filter_not_event_uuid(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t() + @spec filter_not_event_uuid(Ecto.Queryable.t(), String.t() | nil) :: Ecto.Query.t() defp filter_not_event_uuid(query, nil), do: query defp filter_not_event_uuid(query, not_event_uuid) do from(e in query, where: e.uuid != ^not_event_uuid) end - @spec filter_draft(Ecto.Query.t(), boolean) :: Ecto.Query.t() + @spec filter_draft(Ecto.Queryable.t(), boolean) :: Ecto.Query.t() defp filter_draft(query, is_draft \\ false) do from(e in query, where: e.draft == ^is_draft) end - @spec filter_cancelled_events(Ecto.Query.t(), boolean()) :: Ecto.Query.t() - defp filter_cancelled_events(query, hide_cancelled \\ true) - - defp filter_cancelled_events(query, false), do: query - - defp filter_cancelled_events(query, true) do + @spec filter_cancelled_events(Ecto.Queryable.t()) :: Ecto.Query.t() + defp filter_cancelled_events(query) do from(e in query, where: e.status != ^:cancelled) end - @spec filter_future_events(Ecto.Query.t(), boolean) :: Ecto.Query.t() + @spec filter_future_events(Ecto.Queryable.t(), boolean) :: Ecto.Query.t() defp filter_future_events(query, true) do from(q in query, where: q.begins_on > ^DateTime.utc_now() @@ -1713,12 +1715,12 @@ defmodule Mobilizon.Events do defp filter_future_events(query, false), do: query - @spec filter_local(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_local(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_local(query) do where(query, [q], q.local == true) end - @spec filter_local_or_from_followed_instances_events(Ecto.Query.t()) :: + @spec filter_local_or_from_followed_instances_events(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_local_or_from_followed_instances_events(query) do follower_actor_id = Mobilizon.Config.relay_actor_id() @@ -1732,22 +1734,22 @@ defmodule Mobilizon.Events do ) end - @spec filter_approved_role(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_approved_role(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_approved_role(query) do filter_role(query, [:not_approved, :rejected]) end - @spec filter_participant_role(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_participant_role(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_participant_role(query) do filter_role(query, :participant) end - @spec filter_rejected_role(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_rejected_role(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_rejected_role(query) do filter_role(query, :rejected) end - @spec filter_role(Ecto.Query.t(), list(atom())) :: Ecto.Query.t() + @spec filter_role(Ecto.Queryable.t(), list(atom()) | atom()) :: Ecto.Query.t() def filter_role(query, []), do: query def filter_role(query, roles) when is_list(roles) do @@ -1829,6 +1831,6 @@ defmodule Mobilizon.Events do defp participation_order_begins_on_desc(query), do: order_by(query, [_p, e, _a], desc: e.begins_on) - @spec preload_for_event(Ecto.Query.t()) :: Ecto.Query.t() + @spec preload_for_event(Ecto.Queryable.t()) :: Ecto.Query.t() defp preload_for_event(query), do: preload(query, ^@event_preloads) end diff --git a/lib/mobilizon/events/feed_token.ex b/lib/mobilizon/events/feed_token.ex index d2f255db..68b1e29d 100644 --- a/lib/mobilizon/events/feed_token.ex +++ b/lib/mobilizon/events/feed_token.ex @@ -12,7 +12,7 @@ defmodule Mobilizon.Events.FeedToken do @type t :: %__MODULE__{ token: Ecto.UUID.t(), - actor: Actor.t(), + actor: Actor.t() | nil, user: User.t() } @@ -31,7 +31,7 @@ defmodule Mobilizon.Events.FeedToken do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = feed_token, attrs) do feed_token |> cast(attrs, @attrs) diff --git a/lib/mobilizon/events/participant.ex b/lib/mobilizon/events/participant.ex index b490a379..c5c162cb 100644 --- a/lib/mobilizon/events/participant.ex +++ b/lib/mobilizon/events/participant.ex @@ -57,7 +57,7 @@ defmodule Mobilizon.Events.Participant do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = participant, attrs) do participant |> cast(attrs, @attrs) diff --git a/lib/mobilizon/events/participant_metadata.ex b/lib/mobilizon/events/participant_metadata.ex index bca00a0c..279575f9 100644 --- a/lib/mobilizon/events/participant_metadata.ex +++ b/lib/mobilizon/events/participant_metadata.ex @@ -27,7 +27,7 @@ defmodule Mobilizon.Events.Participant.Metadata do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(schema, params) do schema |> cast(params, @attrs) diff --git a/lib/mobilizon/events/session.ex b/lib/mobilizon/events/session.ex index 3804d893..efceb755 100644 --- a/lib/mobilizon/events/session.ex +++ b/lib/mobilizon/events/session.ex @@ -56,7 +56,7 @@ defmodule Mobilizon.Events.Session do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = session, attrs) do session |> cast(attrs, @attrs) diff --git a/lib/mobilizon/events/tag.ex b/lib/mobilizon/events/tag.ex index b4fc428e..128c2388 100644 --- a/lib/mobilizon/events/tag.ex +++ b/lib/mobilizon/events/tag.ex @@ -29,7 +29,7 @@ defmodule Mobilizon.Events.Tag do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = tag, attrs) do tag |> cast(attrs, @attrs) diff --git a/lib/mobilizon/events/tag_relation.ex b/lib/mobilizon/events/tag_relation.ex index 2d038358..91c7b4b0 100644 --- a/lib/mobilizon/events/tag_relation.ex +++ b/lib/mobilizon/events/tag_relation.ex @@ -28,7 +28,7 @@ defmodule Mobilizon.Events.TagRelation do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = tag, attrs) do # Return if tag_id or link_id are not set because it will fail later otherwise with %Ecto.Changeset{errors: [], changes: changes} = changeset <- diff --git a/lib/mobilizon/events/track.ex b/lib/mobilizon/events/track.ex index dd626a82..d2802e15 100644 --- a/lib/mobilizon/events/track.ex +++ b/lib/mobilizon/events/track.ex @@ -33,7 +33,7 @@ defmodule Mobilizon.Events.Track do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = track, attrs) do track |> cast(attrs, @attrs) diff --git a/lib/mobilizon/medias/file.ex b/lib/mobilizon/medias/file.ex index ada4d15a..5efa28e1 100644 --- a/lib/mobilizon/medias/file.ex +++ b/lib/mobilizon/medias/file.ex @@ -29,7 +29,7 @@ defmodule Mobilizon.Medias.File do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = file, attrs) do file |> cast(attrs, @attrs) diff --git a/lib/mobilizon/medias/media.ex b/lib/mobilizon/medias/media.ex index 5feb5e94..bda41a9d 100644 --- a/lib/mobilizon/medias/media.ex +++ b/lib/mobilizon/medias/media.ex @@ -42,7 +42,7 @@ defmodule Mobilizon.Medias.Media do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = media, attrs) do media |> cast(attrs, [:actor_id]) diff --git a/lib/mobilizon/medias/medias.ex b/lib/mobilizon/medias/medias.ex index 7b436262..a706e8b1 100644 --- a/lib/mobilizon/medias/medias.ex +++ b/lib/mobilizon/medias/medias.ex @@ -130,7 +130,7 @@ defmodule Mobilizon.Medias do |> Multi.run(:remove, fn _repo, %{media: %Media{file: %File{url: url}} = media} -> case Upload.remove(url) do {:error, err} -> - if err =~ "doesn't exist" and Keyword.get(opts, :ignore_file_not_found, false) do + if err == :enofile and Keyword.get(opts, :ignore_file_not_found, false) do Logger.info("Deleting media and ignoring absent file.") {:ok, media} else diff --git a/lib/mobilizon/mentions/mention.ex b/lib/mobilizon/mentions/mention.ex index a1473d22..952d91e4 100644 --- a/lib/mobilizon/mentions/mention.ex +++ b/lib/mobilizon/mentions/mention.ex @@ -31,6 +31,7 @@ defmodule Mobilizon.Mention do end @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(event, attrs) do event |> cast(attrs, @attrs) diff --git a/lib/mobilizon/posts/post.ex b/lib/mobilizon/posts/post.ex index cb40587b..bbf43586 100644 --- a/lib/mobilizon/posts/post.ex +++ b/lib/mobilizon/posts/post.ex @@ -82,6 +82,7 @@ defmodule Mobilizon.Posts.Post do @attrs @required_attrs ++ @optional_attrs @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = post, attrs) do post |> cast(attrs, @attrs) @@ -153,17 +154,20 @@ defmodule Mobilizon.Posts.Post do # In case the provided picture is an existing one @spec put_picture(Changeset.t(), map) :: Changeset.t() defp put_picture(%Changeset{} = changeset, %{picture: %{picture_id: id} = _picture}) do - case Medias.get_media!(id) do - %Media{} = picture -> - put_assoc(changeset, :picture, picture) - - _ -> - changeset - end + %Media{} = picture = Medias.get_media!(id) + put_assoc(changeset, :picture, picture) end # In case it's a new picture defp put_picture(%Changeset{} = changeset, _attrs) do cast_assoc(changeset, :picture) end + + @doc """ + Whether we can show the post. Returns false if the organizer actor or group is suspended + """ + @spec show?(t) :: boolean() + def show?(%__MODULE__{attributed_to: %Actor{suspended: true}}), do: false + def show?(%__MODULE__{author: %Actor{suspended: true}}), do: false + def show?(%__MODULE__{}), do: true end diff --git a/lib/mobilizon/posts/posts.ex b/lib/mobilizon/posts/posts.ex index e967e1ee..d7bd9294 100644 --- a/lib/mobilizon/posts/posts.ex +++ b/lib/mobilizon/posts/posts.ex @@ -21,6 +21,7 @@ defmodule Mobilizon.Posts do :private ]) + @spec list_public_local_posts(integer | nil, integer | nil) :: Page.t(Post.t()) def list_public_local_posts(page \\ nil, limit \\ nil) do Post |> filter_public() @@ -144,12 +145,12 @@ defmodule Mobilizon.Posts do ) end - @spec filter_public(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_public(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_public(query) do where(query, [p], p.visibility == ^:public and not p.draft) end - @spec filter_local(Ecto.Query.t()) :: Ecto.Query.t() + @spec filter_local(Ecto.Queryable.t()) :: Ecto.Query.t() defp filter_local(query) do where(query, [q], q.local == true) end @@ -161,7 +162,7 @@ defmodule Mobilizon.Posts do |> preload_post_associations() end - @spec preload_post_associations(Ecto.Query.t(), list()) :: Ecto.Query.t() + @spec preload_post_associations(Ecto.Queryable.t(), list()) :: Ecto.Query.t() defp preload_post_associations(query, associations \\ @post_preloads) do preload(query, ^associations) end diff --git a/lib/mobilizon/reports/note.ex b/lib/mobilizon/reports/note.ex index 560f6246..2ef99861 100644 --- a/lib/mobilizon/reports/note.ex +++ b/lib/mobilizon/reports/note.ex @@ -32,7 +32,7 @@ defmodule Mobilizon.Reports.Note do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = note, attrs) do note |> cast(attrs, @attrs) diff --git a/lib/mobilizon/reports/report.ex b/lib/mobilizon/reports/report.ex index 20e556a6..b9c449a5 100644 --- a/lib/mobilizon/reports/report.ex +++ b/lib/mobilizon/reports/report.ex @@ -56,7 +56,7 @@ defmodule Mobilizon.Reports.Report do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = report, attrs) do report |> cast(attrs, @attrs) diff --git a/lib/mobilizon/resources/resource.ex b/lib/mobilizon/resources/resource.ex index cf2b7520..5c1de9d3 100644 --- a/lib/mobilizon/resources/resource.ex +++ b/lib/mobilizon/resources/resource.ex @@ -79,6 +79,7 @@ defmodule Mobilizon.Resources.Resource do ] @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(resource, attrs) do resource |> cast(attrs, @attrs) diff --git a/lib/mobilizon/share.ex b/lib/mobilizon/share.ex index 73462834..03924b40 100644 --- a/lib/mobilizon/share.ex +++ b/lib/mobilizon/share.ex @@ -29,6 +29,7 @@ defmodule Mobilizon.Share do end @doc false + @spec changeset(t | Ecto.Schema.t(), map()) :: Ecto.Changeset.t() def changeset(share, attrs) do share |> cast(attrs, @attrs) diff --git a/lib/mobilizon/storage/page.ex b/lib/mobilizon/storage/page.ex index 68fc8aab..43b9ebc5 100644 --- a/lib/mobilizon/storage/page.ex +++ b/lib/mobilizon/storage/page.ex @@ -12,9 +12,9 @@ defmodule Mobilizon.Storage.Page do :elements ] - @type t :: %__MODULE__{ + @type t(structure) :: %__MODULE__{ total: integer, - elements: struct + elements: list(structure) } @doc """ @@ -23,7 +23,7 @@ defmodule Mobilizon.Storage.Page do `field` is use to define the field that will be used for the count aggregate, which should be the same as the field used for order_by See https://stackoverflow.com/q/12693089/10204399 """ - @spec build_page(Ecto.Query.t(), integer | nil, integer | nil, atom()) :: t + @spec build_page(Ecto.Queryable.t(), integer | nil, integer | nil, atom()) :: t(any) def build_page(query, page, limit, field \\ :id) do [total, elements] = [ @@ -39,7 +39,7 @@ defmodule Mobilizon.Storage.Page do @doc """ Add limit and offset to the query. """ - @spec paginate(Ecto.Query.t() | struct, integer | nil, integer | nil) :: Ecto.Query.t() + @spec paginate(Ecto.Queryable.t() | struct, integer | nil, integer | nil) :: Ecto.Query.t() def paginate(query, page \\ 1, size \\ 10) def paginate(query, page, _size) when is_nil(page), do: paginate(query) diff --git a/lib/mobilizon/todos/todo.ex b/lib/mobilizon/todos/todo.ex index 82ed0906..fbc4be92 100644 --- a/lib/mobilizon/todos/todo.ex +++ b/lib/mobilizon/todos/todo.ex @@ -40,6 +40,7 @@ defmodule Mobilizon.Todos.Todo do @attrs @required_attrs ++ @optional_attrs @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(todo, attrs) do todo |> cast(attrs, @attrs) diff --git a/lib/mobilizon/todos/todo_list.ex b/lib/mobilizon/todos/todo_list.ex index 5ef31afb..9f27ac28 100644 --- a/lib/mobilizon/todos/todo_list.ex +++ b/lib/mobilizon/todos/todo_list.ex @@ -34,6 +34,7 @@ defmodule Mobilizon.Todos.TodoList do @attrs @required_attrs ++ @optional_attrs @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(todo_list, attrs) do todo_list |> cast(attrs, @attrs) diff --git a/lib/mobilizon/tombstone.ex b/lib/mobilizon/tombstone.ex index 7b464b6c..62beb6f9 100644 --- a/lib/mobilizon/tombstone.ex +++ b/lib/mobilizon/tombstone.ex @@ -26,20 +26,21 @@ defmodule Mobilizon.Tombstone do end @doc false + @spec changeset(t | Ecto.Schema.t(), map()) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = tombstone, attrs) do tombstone |> cast(attrs, @attrs) |> validate_required(@required_attrs) end - @spec create_tombstone(map) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + @spec create_tombstone(map) :: {:ok, t()} | {:error, Ecto.Changeset.t()} def create_tombstone(attrs) do %__MODULE__{} |> changeset(attrs) |> Repo.insert(on_conflict: :replace_all, conflict_target: :uri) end - @spec find_tombstone(String.t()) :: Ecto.Schema.t() | nil + @spec find_tombstone(String.t()) :: t() | nil def find_tombstone(uri) do __MODULE__ |> Ecto.Query.where([t], t.uri == ^uri) @@ -54,6 +55,7 @@ defmodule Mobilizon.Tombstone do |> Repo.delete_all() end + @spec delete_uri_tombstone(String.t()) :: {integer(), nil} def delete_uri_tombstone(uri) do __MODULE__ |> Ecto.Query.where(uri: ^uri) diff --git a/lib/mobilizon/users/activity_setting.ex b/lib/mobilizon/users/activity_setting.ex index 12f956c5..acc5db42 100644 --- a/lib/mobilizon/users/activity_setting.ex +++ b/lib/mobilizon/users/activity_setting.ex @@ -25,6 +25,7 @@ defmodule Mobilizon.Users.ActivitySetting do end @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(activity_setting, attrs) do activity_setting |> cast(attrs, @attrs) diff --git a/lib/mobilizon/users/push_subscription.ex b/lib/mobilizon/users/push_subscription.ex index be94336e..d70433eb 100644 --- a/lib/mobilizon/users/push_subscription.ex +++ b/lib/mobilizon/users/push_subscription.ex @@ -25,6 +25,7 @@ defmodule Mobilizon.Users.PushSubscription do end @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(push_subscription, attrs) do push_subscription |> cast(attrs, [:user_id, :endpoint, :auth, :p256dh]) diff --git a/lib/mobilizon/users/setting.ex b/lib/mobilizon/users/setting.ex index b4f7d01d..9abb9075 100644 --- a/lib/mobilizon/users/setting.ex +++ b/lib/mobilizon/users/setting.ex @@ -19,6 +19,12 @@ defmodule Mobilizon.Users.Setting do user: User.t() } + @type location :: %{ + name: String.t(), + range: integer, + geohash: String.t() + } + @required_attrs [:user_id] @optional_attrs [ @@ -66,6 +72,7 @@ defmodule Mobilizon.Users.Setting do end @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(setting, attrs) do setting |> cast(attrs, @attrs) @@ -73,6 +80,7 @@ defmodule Mobilizon.Users.Setting do |> validate_required(@required_attrs) end + @spec location_changeset(location, map) :: Ecto.Changeset.t() def location_changeset(schema, params) do schema |> cast(params, @location_attrs) diff --git a/lib/mobilizon/users/user.ex b/lib/mobilizon/users/user.ex index 3cfdaaa8..78427214 100644 --- a/lib/mobilizon/users/user.ex +++ b/lib/mobilizon/users/user.ex @@ -19,7 +19,7 @@ defmodule Mobilizon.Users.User do password_hash: String.t(), password: String.t(), role: UserRole.t(), - confirmed_at: DateTime.t(), + confirmed_at: DateTime.t() | nil, confirmation_sent_at: DateTime.t(), confirmation_token: String.t(), reset_password_sent_at: DateTime.t(), @@ -32,7 +32,10 @@ defmodule Mobilizon.Users.User do last_sign_in_at: DateTime.t(), last_sign_in_ip: String.t(), current_sign_in_ip: String.t(), - current_sign_in_at: DateTime.t() + current_sign_in_at: DateTime.t(), + activity_settings: [ActivitySetting.t()], + settings: Setting.t(), + unconfirmed_email: String.t() | nil } @required_attrs [:email] @@ -96,7 +99,7 @@ defmodule Mobilizon.Users.User do end @doc false - @spec changeset(t, map) :: Ecto.Changeset.t() + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = user, attrs) do changeset = user @@ -129,7 +132,6 @@ defmodule Mobilizon.Users.User do def registration_changeset(%__MODULE__{} = user, attrs) do user |> changeset(attrs) - |> cast_assoc(:default_actor) |> validate_required(@registration_required_attrs) |> hash_password() |> save_confirmation_token() @@ -148,7 +150,6 @@ defmodule Mobilizon.Users.User do def auth_provider_changeset(%__MODULE__{} = user, attrs) do user |> changeset(attrs) - |> cast_assoc(:default_actor) |> put_change(:confirmed_at, DateTime.utc_now() |> DateTime.truncate(:second)) |> validate_required(@auth_provider_required_attrs) end diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex index aee2742e..b19ecffd 100644 --- a/lib/mobilizon/users/users.ex +++ b/lib/mobilizon/users/users.ex @@ -338,10 +338,10 @@ defmodule Mobilizon.Users do """ def get_setting!(user_id), do: Repo.get!(Setting, user_id) - @spec get_setting(User.t()) :: Setting.t() + @spec get_setting(User.t()) :: Setting.t() | nil def get_setting(%User{id: user_id}), do: get_setting(user_id) - @spec get_setting(String.t() | integer()) :: Setting.t() + @spec get_setting(String.t() | integer()) :: Setting.t() | nil def get_setting(user_id), do: Repo.get(Setting, user_id) @doc """ @@ -356,6 +356,7 @@ defmodule Mobilizon.Users do {:error, %Ecto.Changeset{}} """ + @spec create_setting(map()) :: {:ok, Setting.t()} | {:error, Ecto.Changeset.t()} def create_setting(attrs \\ %{}) do %Setting{} |> Setting.changeset(attrs) @@ -445,6 +446,8 @@ defmodule Mobilizon.Users do {:error, %Ecto.Changeset{}} """ + @spec create_push_subscription(map()) :: + {:ok, PushSubscription.t()} | {:error, Ecto.Changeset.t()} def create_push_subscription(attrs \\ %{}) do %PushSubscription{} |> PushSubscription.changeset(attrs) diff --git a/lib/service/activity/comment.ex b/lib/service/activity/comment.ex index b2d8b406..2c62e00b 100644 --- a/lib/service/activity/comment.ex +++ b/lib/service/activity/comment.ex @@ -51,11 +51,11 @@ defmodule Mobilizon.Service.Activity.Comment do "object_type" => :comment } + @spec handle_notification(Keyword.t(), notification_type, Comment.t(), Event.t(), Keyword.t()) :: + Keyword.t() defp handle_notification(global_res, function, comment, event, options) do - case notify(function, comment, event, options) do - {:ok, res} -> Keyword.put(global_res, function, res) - _ -> Keyword.put(global_res, function, :error) - end + {:ok, res} = notify(function, comment, event, options) + Keyword.put(global_res, function, res) end @spec legacy_notifier_enqueue(map()) :: :ok @@ -66,11 +66,11 @@ defmodule Mobilizon.Service.Activity.Comment do ) end - @type notification_type :: atom() + @type notification_type :: :mentionned | :announcement | :organizer # An actor is mentionned @spec notify(notification_type(), Comment.t(), Event.t(), Keyword.t()) :: - {:ok, Oban.Job.t()} | {:ok, :skipped} + {:ok, :enqueued} | {:ok, :skipped} defp notify( :mentionned, %Comment{actor_id: actor_id, id: comment_id, mentions: mentions}, diff --git a/lib/service/activity/group.ex b/lib/service/activity/group.ex index 5a18a7c8..23454021 100644 --- a/lib/service/activity/group.ex +++ b/lib/service/activity/group.ex @@ -10,31 +10,35 @@ defmodule Mobilizon.Service.Activity.Group do @behaviour Activity @impl Activity + @spec insert_activity(Actor.t(), Keyword.t()) :: + {:ok, Job.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()} def insert_activity(group, options \\ []) def insert_activity( %Actor{type: :Group, id: group_id}, options ) do - with %Actor{type: :Group} = group <- Actors.get_actor(group_id), - subject when not is_nil(subject) <- Keyword.get(options, :subject), - actor_id <- Keyword.get(options, :actor_id), - default_updater_actor <- Actors.get_actor(actor_id), - %Actor{id: actor_id} <- - Keyword.get(options, :updater_actor, default_updater_actor), - old_group <- Keyword.get(options, :old_group) do - ActivityBuilder.enqueue(:build_activity, %{ - "type" => "group", - "subject" => subject, - "subject_params" => subject_params(group, subject, old_group), - "group_id" => group.id, - "author_id" => actor_id, - "object_type" => "group", - "object_id" => to_string(group.id), - "inserted_at" => DateTime.utc_now() - }) - else - _ -> {:ok, nil} + subject = Keyword.get(options, :subject) + actor_id = Keyword.get(options, :actor_id) + default_updater_actor = Actors.get_actor(actor_id) + %Actor{id: actor_id} = Keyword.get(options, :updater_actor, default_updater_actor) + old_group = Keyword.get(options, :old_group) + + case Actors.get_actor(group_id) do + %Actor{type: :Group} = group -> + ActivityBuilder.enqueue(:build_activity, %{ + "type" => "group", + "subject" => subject, + "subject_params" => subject_params(group, subject, old_group), + "group_id" => group.id, + "author_id" => actor_id, + "object_type" => "group", + "object_id" => to_string(group.id), + "inserted_at" => DateTime.utc_now() + }) + + nil -> + {:ok, nil} end end diff --git a/lib/service/activity/utils.ex b/lib/service/activity/utils.ex index 6441ea6c..ecfd33b6 100644 --- a/lib/service/activity/utils.ex +++ b/lib/service/activity/utils.ex @@ -12,9 +12,9 @@ defmodule Mobilizon.Service.Activity.Utils do |> add_activity_object() end - @spec add_activity_object(Activity.t()) :: map() + @spec add_activity_object(Activity.t()) :: Activity.t() def add_activity_object(%Activity{} = activity) do - Map.put(activity, :object, ActivityService.object(activity)) + %Activity{activity | object: ActivityService.object(activity)} end @spec transform_params(map()) :: list() diff --git a/lib/service/address/address.ex b/lib/service/address/address.ex index c4edc502..59cc8682 100644 --- a/lib/service/address/address.ex +++ b/lib/service/address/address.ex @@ -7,6 +7,7 @@ defmodule Mobilizon.Service.Address do @type address :: %{name: String.t(), alternative_name: String.t()} + @spec render_address(AddressModel.t()) :: String.t() | no_return def render_address(%AddressModel{} = address) do %{name: name, alternative_name: alternative_name} = render_names(address) diff --git a/lib/service/clean_old_activity.ex b/lib/service/clean_old_activity.ex index 8801eee5..7c294a09 100644 --- a/lib/service/clean_old_activity.ex +++ b/lib/service/clean_old_activity.ex @@ -31,7 +31,7 @@ defmodule Mobilizon.Service.CleanOldActivity do end end - @spec find_activities(Keyword.t()) :: {Ecto.Query.t(), list()} + @spec find_activities(Keyword.t()) :: {Ecto.Query.t(), integer()} defp find_activities(opts) do grace_period = Keyword.get(opts, :grace_period, Config.get([:instance, :activity_expire_days], 365)) diff --git a/lib/service/clean_unconfirmed_users.ex b/lib/service/clean_unconfirmed_users.ex index d9e6e9ea..28c77d39 100644 --- a/lib/service/clean_unconfirmed_users.ex +++ b/lib/service/clean_unconfirmed_users.ex @@ -3,9 +3,10 @@ defmodule Mobilizon.Service.CleanUnconfirmedUsers do Service to clean unconfirmed users """ - alias Mobilizon.{Actors, Users} alias Mobilizon.Federation.ActivityPub.Relay + alias Mobilizon.Service.ActorSuspension alias Mobilizon.Storage.Repo + alias Mobilizon.Users alias Mobilizon.Users.User import Ecto.Query @@ -27,21 +28,21 @@ defmodule Mobilizon.Service.CleanUnconfirmedUsers do end end - @spec delete_user(User.t()) :: {:ok, User.t()} + @spec delete_user(User.t()) :: User.t() | {:error, Ecto.Changeset.t()} | no_return defp delete_user(%User{} = user) do - with actors <- Users.get_actors_for_user(user), - :ok <- - Enum.each(actors, fn actor -> - actor_performing = Relay.get_actor() + actors = Users.get_actors_for_user(user) + %{id: actor_performing_id} = Relay.get_actor() - Actors.perform(:delete_actor, actor, - author_id: actor_performing.id, - reserve_username: false - ) - end), - {:ok, %User{} = user} <- Users.delete_user(user, reserve_email: false), - %User{} = user <- %User{user | actors: actors} do - user + Enum.each(actors, fn actor -> + ActorSuspension.suspend_actor(actor, + author_id: actor_performing_id, + reserve_username: false + ) + end) + + case Users.delete_user(user, reserve_email: false) do + {:ok, %User{} = user} -> + %User{user | actors: actors} end end diff --git a/lib/service/date_time/date_time.ex b/lib/service/date_time/date_time.ex index ed58b689..039da30c 100644 --- a/lib/service/date_time/date_time.ex +++ b/lib/service/date_time/date_time.ex @@ -4,10 +4,14 @@ defmodule Mobilizon.Service.DateTime do """ alias Cldr.DateTime.Relative + @typep to_string_format :: :short | :medium | :long | :full + + @spec datetime_to_string(DateTime.t(), String.t(), to_string_format()) :: String.t() def datetime_to_string(%DateTime{} = datetime, locale \\ "en", format \\ :medium) do Mobilizon.Cldr.DateTime.to_string!(datetime, format: format, locale: locale_or_default(locale)) end + @spec datetime_to_time_string(DateTime.t(), String.t(), to_string_format()) :: String.t() def datetime_to_time_string(%DateTime{} = datetime, locale \\ "en", format \\ :short) do Mobilizon.Cldr.Time.to_string!(datetime, format: format, locale: locale_or_default(locale)) end @@ -39,7 +43,8 @@ defmodule Mobilizon.Service.DateTime do end end - def is_first_day_of_week(%Date{} = date, locale \\ "en") do + @spec is_first_day_of_week(Date.t(), String.t()) :: boolean() + defp is_first_day_of_week(%Date{} = date, locale) do Date.day_of_week(date) == Cldr.Calendar.first_day_for_locale(locale) end @@ -119,17 +124,18 @@ defmodule Mobilizon.Service.DateTime do compare_to |> DateTime.to_date() |> calculate_first_day_of_week(locale) - |> Timex.add(Timex.Duration.from_weeks(1)) + |> Date.add(7) |> build_notification_datetime(options) - if Date.compare(datetime, next_first_day_of_week) == :gt do + if next_first_day_of_week != nil && DateTime.compare(datetime, next_first_day_of_week) == :gt do next_first_day_of_week else nil end end - def appropriate_first_day_of_week(%DateTime{} = datetime, options) do + @spec appropriate_first_day_of_week(DateTime.t(), keyword) :: DateTime.t() | nil + defp appropriate_first_day_of_week(%DateTime{} = datetime, options) do locale = Keyword.get(options, :locale, "en") timezone = Keyword.get(options, :timezone, "Etc/UTC") @@ -141,15 +147,20 @@ defmodule Mobilizon.Service.DateTime do if DateTime.compare(local_datetime, first_datetime) == :gt do first_datetime else - next_first_day_of_week(local_datetime, options) + local_datetime + |> next_first_day_of_week(options) + |> build_notification_datetime(options) end end @spec build_notification_datetime(Date.t(), Keyword.t()) :: DateTime.t() - def build_notification_datetime( - %Date{} = date, - options - ) do + @spec build_notification_datetime(nil, Keyword.t()) :: nil + defp build_notification_datetime(nil, _options), do: nil + + defp build_notification_datetime( + %Date{} = date, + options + ) do notification_time = Keyword.get(options, :notification_time, ~T[08:00:00]) timezone = Keyword.get(options, :timezone, "Etc/UTC") DateTime.new!(date, notification_time, timezone) @@ -158,8 +169,8 @@ defmodule Mobilizon.Service.DateTime do @start_time ~T[08:00:00] @end_time ~T[09:00:00] - @spec is_between_hours(Keyword.t()) :: boolean() - def is_between_hours(options \\ []) when is_list(options) do + @spec is_between_hours?(Keyword.t()) :: boolean() + def is_between_hours?(options \\ []) when is_list(options) do compare_to_day = Keyword.get(options, :compare_to_day, Date.utc_today()) compare_to = Keyword.get(options, :compare_to_datetime, DateTime.utc_now()) start_time = Keyword.get(options, :start_time, @start_time) @@ -176,17 +187,16 @@ defmodule Mobilizon.Service.DateTime do ) == :lt end - @spec is_between_hours_on_first_day(Keyword.t()) :: boolean() - def is_between_hours_on_first_day(options) when is_list(options) do + @spec is_between_hours_on_first_day?(Keyword.t()) :: boolean() + def is_between_hours_on_first_day?(options) when is_list(options) do compare_to_day = Keyword.get(options, :compare_to_day, Date.utc_today()) locale = Keyword.get(options, :locale, "en") - Mobilizon.Service.DateTime.is_first_day_of_week(compare_to_day, locale) && - is_between_hours(options) + is_first_day_of_week(compare_to_day, locale) && is_between_hours?(options) end - @spec is_delay_ok_since_last_notification_sent(DateTime.t()) :: boolean() - def is_delay_ok_since_last_notification_sent(%DateTime{} = last_notification_sent) do + @spec is_delay_ok_since_last_notification_sent?(DateTime.t()) :: boolean() + def is_delay_ok_since_last_notification_sent?(%DateTime{} = last_notification_sent) do DateTime.compare(DateTime.add(last_notification_sent, 3_600), DateTime.utc_now()) == :lt end diff --git a/lib/service/error_reporting/sentry.ex b/lib/service/error_reporting/sentry.ex index d9f23464..333b8e57 100644 --- a/lib/service/error_reporting/sentry.ex +++ b/lib/service/error_reporting/sentry.ex @@ -29,6 +29,7 @@ defmodule Mobilizon.Service.ErrorReporting.Sentry do end @impl ErrorReporting + @spec attach :: :ok | {:error, :already_exists} def attach do :telemetry.attach( "oban-errors", diff --git a/lib/service/export/common.ex b/lib/service/export/common.ex index c414fd9d..8b11870b 100644 --- a/lib/service/export/common.ex +++ b/lib/service/export/common.ex @@ -6,56 +6,99 @@ defmodule Mobilizon.Service.Export.Common do alias Mobilizon.{Actors, Events, Posts, Users} alias Mobilizon.Actors.Actor alias Mobilizon.Events.{Event, FeedToken} + alias Mobilizon.Posts.Post alias Mobilizon.Storage.Page alias Mobilizon.Users.User - @spec fetch_actor_event_feed(String.t(), integer()) :: String.t() + @spec fetch_actor_event_feed(String.t(), integer()) :: + {:ok, Actor.t(), [Event.t()], [Post.t()]} + | {:error, :actor_not_public | :actor_not_found} def fetch_actor_event_feed(name, limit) do - with %Actor{} = actor <- Actors.get_actor_by_name(name), - {:visibility, true} <- {:visibility, Actor.is_public_visibility?(actor)}, - %Page{elements: events} <- Events.list_public_events_for_actor(actor, 1, limit), - %Page{elements: posts} <- Posts.get_public_posts_for_group(actor, 1, limit) do - {:ok, actor, events, posts} - else - err -> - {:error, err} + case Actors.get_actor_by_name(name) do + %Actor{} = actor -> + if Actor.is_public_visibility?(actor) do + %Page{elements: events} = Events.list_public_events_for_actor(actor, 1, limit) + %Page{elements: posts} = Posts.get_public_posts_for_group(actor, 1, limit) + {:ok, actor, events, posts} + else + {:error, :actor_not_public} + end + + nil -> + {:error, :actor_not_found} end end - # Only events, not posts - @spec fetch_events_from_token(String.t(), integer()) :: String.t() - def fetch_events_from_token(token, limit) do - with {:ok, uuid} <- ShortUUID.decode(token), - {:ok, _uuid} <- Ecto.UUID.cast(uuid), - %FeedToken{actor: actor, user: %User{} = user} <- Events.get_feed_token(uuid) do - case actor do - %Actor{} = actor -> - %{ - type: :actor, - actor: actor, - events: fetch_actor_private_events(actor, limit), - user: user, - token: token - } + @typep token_feed_data :: %{ + type: :actor | :user, + actor: Actor.t() | nil, + user: User.t(), + events: [Event.t()], + token: String.t() + } - nil -> - with actors <- Users.get_actors_for_user(user), - events <- - actors - |> Enum.map(&fetch_actor_private_events(&1, limit)) - |> Enum.concat() do - %{type: :user, events: events, user: user, token: token, actor: nil} - end - end + # Only events, not posts + @spec fetch_events_from_token(String.t(), integer()) :: + token_feed_data | {:error, :bad_token | :token_not_found} + def fetch_events_from_token(token, limit) do + case uuid_from_token(token) do + {:ok, uuid} -> + case Events.get_feed_token(uuid) do + nil -> + {:error, :token_not_found} + + %FeedToken{actor: actor, user: %User{} = user} -> + produce_actor_feed(actor, user, token, limit) + end + + {:error, :bad_token} -> + {:error, :bad_token} + end + end + + @spec uuid_from_token(String.t()) :: {:ok, String.t()} | {:error, :bad_token} + defp uuid_from_token(token) do + case ShortUUID.decode(token) do + {:ok, uuid} -> + case Ecto.UUID.cast(uuid) do + {:ok, _uuid} -> + {:ok, uuid} + + :error -> + {:error, :bad_token} + end + + {:error, _err} -> + {:error, :bad_token} + end + end + + @spec produce_actor_feed(Actor.t() | nil, User.t(), String.t(), integer()) :: token_feed_data + defp produce_actor_feed(%Actor{} = actor, %User{} = user, token, limit) do + %{ + type: :actor, + actor: actor, + events: fetch_actor_private_events(actor, limit), + user: user, + token: token + } + end + + defp produce_actor_feed(nil, %User{} = user, token, limit) do + with actors <- Users.get_actors_for_user(user), + events <- + actors + |> Enum.map(&fetch_actor_private_events(&1, limit)) + |> Enum.concat() do + %{type: :user, events: events, user: user, token: token, actor: nil} end end @spec fetch_instance_public_content(integer()) :: {:ok, list(Event.t()), list(Post.t())} def fetch_instance_public_content(limit) do - with %Page{elements: events} <- Events.list_public_local_events(1, limit), - %Page{elements: posts} <- Posts.list_public_local_posts(1, limit) do - {:ok, events, posts} - end + %Page{elements: events} = Events.list_public_local_events(1, limit) + %Page{elements: posts} = Posts.list_public_local_posts(1, limit) + {:ok, events, posts} end @spec fetch_actor_private_events(Actor.t(), integer()) :: list(Event.t()) diff --git a/lib/service/export/feed.ex b/lib/service/export/feed.ex index 7d764f01..6af40bad 100644 --- a/lib/service/export/feed.ex +++ b/lib/service/export/feed.ex @@ -96,13 +96,13 @@ defmodule Mobilizon.Service.Export.Feed do {:ok, actor, events, posts} -> {:ok, build_actor_feed(actor, events, posts)} - err -> + {:error, err} -> {:error, err} end end # Build an atom feed from actor and its public events and posts - @spec build_actor_feed(Actor.t(), list(), list(), boolean()) :: String.t() + @spec build_actor_feed(Actor.t(), list(Event.t()), list(Post.t()), boolean()) :: String.t() defp build_actor_feed(%Actor{} = actor, events, posts, public \\ true) do display_name = Actor.display_name(actor) @@ -199,19 +199,22 @@ defmodule Mobilizon.Service.Export.Feed do end # Only events, not posts - @spec fetch_events_from_token(String.t()) :: String.t() + @spec fetch_events_from_token(String.t(), integer()) :: {:ok, String.t()} | {:error, atom()} defp fetch_events_from_token(token, limit \\ @item_limit) do - with %{events: events, token: token, user: user, actor: actor, type: type} <- - Common.fetch_events_from_token(token, limit) do - case type do - :user -> {:ok, build_user_feed(events, user, token)} - :actor -> {:ok, build_actor_feed(actor, events, [], false)} - end + case Common.fetch_events_from_token(token, limit) do + %{events: events, token: token, user: user, actor: actor, type: type} -> + case type do + :user -> {:ok, build_user_feed(events, user, token)} + :actor -> {:ok, build_actor_feed(actor, events, [], false)} + end + + {:error, err} -> + {:error, err} end end # Build an atom feed from actor and its public events - @spec build_user_feed(list(), User.t(), String.t()) :: String.t() + @spec build_user_feed(list(Event.t()), User.t(), String.t()) :: String.t() defp build_user_feed(events, %User{email: email}, token) do self_url = Endpoint |> Routes.feed_url(:going, token, "atom") |> URI.decode() diff --git a/lib/service/export/icalendar.ex b/lib/service/export/icalendar.ex index c3ba2dde..74e2ee3d 100644 --- a/lib/service/export/icalendar.ex +++ b/lib/service/export/icalendar.ex @@ -15,12 +15,13 @@ defmodule Mobilizon.Service.Export.ICalendar do @doc """ Create cache for an actor, an event or an user token """ + @spec create_cache(String.t()) :: {:commit, String.t()} | {:ignore, atom()} def create_cache("actor_" <> name) do case export_public_actor(name) do {:ok, res} -> {:commit, res} - err -> + {:error, err} -> {:ignore, err} end end @@ -30,8 +31,11 @@ defmodule Mobilizon.Service.Export.ICalendar do {:ok, res} <- export_public_event(event) do {:commit, res} else - err -> + {:error, err} -> {:ignore, err} + + nil -> + {:ignore, :event_not_found} end end @@ -40,30 +44,20 @@ defmodule Mobilizon.Service.Export.ICalendar do {:ok, res} -> {:commit, res} - err -> + {:error, err} -> {:ignore, err} end end def create_cache("instance") do - case fetch_instance_feed() do - {:ok, res} -> - {:commit, res} - - err -> - {:ignore, err} - end + {:ok, res} = fetch_instance_feed() + {:commit, res} end @spec fetch_instance_feed :: {:ok, String.t()} defp fetch_instance_feed do - case Common.fetch_instance_public_content(@item_limit) do - {:ok, events, _posts} -> - {:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()} - - err -> - {:error, err} - end + {:ok, events, _posts} = Common.fetch_instance_public_content(@item_limit) + {:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()} end @doc """ @@ -77,13 +71,12 @@ defmodule Mobilizon.Service.Export.ICalendar do The event must have a visibility of `:public` or `:unlisted` """ - @spec export_public_event(Event.t()) :: {:ok, String.t()} + @spec export_public_event(Event.t()) :: {:ok, String.t()} | {:error, :event_not_public} def export_public_event(%Event{visibility: visibility} = event) when visibility in [:public, :unlisted] do {:ok, events_to_ics([event])} end - @spec export_public_event(Event.t()) :: {:error, :event_not_public} def export_public_event(%Event{}), do: {:error, :event_not_public} @doc """ @@ -91,28 +84,33 @@ defmodule Mobilizon.Service.Export.ICalendar do The actor must have a visibility of `:public` or `:unlisted`, as well as the events """ - @spec export_public_actor(String.t()) :: String.t() + @spec export_public_actor(String.t()) :: + {:ok, String.t()} | {:error, :actor_not_public | :actor_not_found} def export_public_actor(name, limit \\ @item_limit) do case Common.fetch_actor_event_feed(name, limit) do {:ok, _actor, events, _posts} -> {:ok, events_to_ics(events)} - err -> + {:error, err} -> {:error, err} end end - @spec export_private_actor(Actor.t()) :: String.t() + @spec export_private_actor(Actor.t(), integer()) :: {:ok, String.t()} def export_private_actor(%Actor{} = actor, limit \\ @item_limit) do - with events <- Common.fetch_actor_private_events(actor, limit) do - {:ok, events_to_ics(events)} - end + events = Common.fetch_actor_private_events(actor, limit) + {:ok, events_to_ics(events)} end - @spec fetch_events_from_token(String.t()) :: String.t() + @spec fetch_events_from_token(String.t(), integer()) :: + {:ok, String.t()} | {:error, :actor_not_public | :actor_not_found} defp fetch_events_from_token(token, limit \\ @item_limit) do - with %{events: events} <- Common.fetch_events_from_token(token, limit) do - {:ok, events_to_ics(events)} + case Common.fetch_events_from_token(token, limit) do + %{events: events} -> + {:ok, events_to_ics(events)} + + {:error, err} -> + {:error, err} end end @@ -138,6 +136,7 @@ defmodule Mobilizon.Service.Export.ICalendar do } end + @spec vendor :: String.t() defp vendor do "Mobilizon #{Config.instance_version()}" end diff --git a/lib/service/formatter/formatter.ex b/lib/service/formatter/formatter.ex index bba56b74..0249d761 100644 --- a/lib/service/formatter/formatter.ex +++ b/lib/service/formatter/formatter.ex @@ -18,18 +18,20 @@ defmodule Mobilizon.Service.Formatter do @link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/ - def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do + @spec escape_mention_handler(String.t(), String.t(), any(), any()) :: String.t() + defp escape_mention_handler("@" <> nickname = mention, buffer, _, _) do case Actors.get_actor_by_name(nickname) do %Actor{} -> # escape markdown characters with `\\` # (we don't want something like @user__name to be parsed by markdown) String.replace(mention, @markdown_characters_regex, "\\\\\\1") - _ -> + nil -> buffer end end + @spec mention_handler(String.t(), String.t(), any(), map()) :: {String.t(), map()} def mention_handler("@" <> nickname, buffer, _opts, acc) do case Actors.get_actor_by_name(nickname) do # %Actor{preferred_username: preferred_username} = actor -> @@ -58,7 +60,7 @@ defmodule Mobilizon.Service.Formatter do {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}} - _ -> + nil -> {buffer, acc} end end diff --git a/lib/service/formatter/html.ex b/lib/service/formatter/html.ex index 2e8bd5ee..5544eede 100644 --- a/lib/service/formatter/html.ex +++ b/lib/service/formatter/html.ex @@ -14,6 +14,7 @@ defmodule Mobilizon.Service.Formatter.HTML do def filter_tags(html), do: Sanitizer.scrub(html, DefaultScrubbler) + @spec strip_tags(String.t()) :: String.t() | no_return() def strip_tags(html) do case FastSanitize.strip_tags(html) do {:ok, html} -> diff --git a/lib/service/geospatial/addok.ex b/lib/service/geospatial/addok.ex index e289925a..bf3d1f25 100644 --- a/lib/service/geospatial/addok.ex +++ b/lib/service/geospatial/addok.ex @@ -93,7 +93,8 @@ defmodule Mobilizon.Service.Geospatial.Addok do if is_nil(value), do: url, else: do_add_parameter(url, key, value) end - @spec do_add_parameter(String.t(), atom(), any()) :: String.t() + @spec do_add_parameter(String.t(), :coords | :type, %{lat: float, lon: float} | :administrative) :: + String.t() defp do_add_parameter(url, :coords, coords), do: "#{url}&lat=#{coords.lat}&lon=#{coords.lon}" diff --git a/lib/service/geospatial/google_maps.ex b/lib/service/geospatial/google_maps.ex index 3e145e6a..0b50c599 100644 --- a/lib/service/geospatial/google_maps.ex +++ b/lib/service/geospatial/google_maps.ex @@ -31,7 +31,7 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do @doc """ Google Maps implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`. """ - @spec geocode(String.t(), keyword()) :: list(Address.t()) + @spec geocode(String.t(), keyword()) :: list(Address.t()) | no_return def geocode(lon, lat, options \\ []) do url = build_url(:geocode, %{lon: lon, lat: lat}, options) @@ -50,7 +50,7 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do @doc """ Google Maps implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`. """ - @spec search(String.t(), keyword()) :: list(Address.t()) + @spec search(String.t(), keyword()) :: list(Address.t()) | no_return def search(q, options \\ []) do url = build_url(:search, %{q: q}, options) @@ -68,7 +68,7 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do end end - @spec build_url(atom(), map(), list()) :: String.t() + @spec build_url(atom(), map(), list()) :: String.t() | no_return defp build_url(method, args, options) do limit = Keyword.get(options, :limit, 10) lang = Keyword.get(options, :lang, "en") @@ -148,6 +148,7 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do end end + @spec do_fetch_place_details(String.t() | nil, Keyword.t()) :: String.t() | no_return defp do_fetch_place_details(place_id, options) do url = build_url(:place_details, %{place_id: place_id}, options) diff --git a/lib/service/geospatial/provider.ex b/lib/service/geospatial/provider.ex index dd2f4d89..47d92f7b 100644 --- a/lib/service/geospatial/provider.ex +++ b/lib/service/geospatial/provider.ex @@ -42,7 +42,8 @@ defmodule Mobilizon.Service.Geospatial.Provider do iex> geocode(48.11, -1.77) %Address{} """ - @callback geocode(longitude :: number, latitude :: number, options :: keyword) :: [Address.t()] + @callback geocode(longitude :: number, latitude :: number, options :: keyword) :: + [Address.t()] | {:error, atom()} @doc """ Search for an address @@ -62,12 +63,12 @@ defmodule Mobilizon.Service.Geospatial.Provider do iex> search("10 rue Jangot") %Address{} """ - @callback search(address :: String.t(), options :: keyword) :: [Address.t()] + @callback search(address :: String.t(), options :: keyword) :: [Address.t()] | {:error, atom()} @doc """ Returns a `Geo.Point` for given coordinates """ - @spec coordinates([number], number) :: Geo.Point.t() | nil + @spec coordinates([number | String.t()], number) :: Geo.Point.t() | nil def coordinates(coords, srid \\ 4326) def coordinates([x, y], srid) when is_number(x) and is_number(y) do diff --git a/lib/service/notifier/email.ex b/lib/service/notifier/email.ex index c12f6106..58388dd9 100644 --- a/lib/service/notifier/email.ex +++ b/lib/service/notifier/email.ex @@ -12,7 +12,7 @@ defmodule Mobilizon.Service.Notifier.Email do import Mobilizon.Service.DateTime, only: [ - is_delay_ok_since_last_notification_sent: 1 + is_delay_ok_since_last_notification_sent?: 1 ] require Logger @@ -116,7 +116,7 @@ defmodule Mobilizon.Service.Notifier.Email do # Delay ok since last notification defp match_group_notifications_setting(:one_hour, _, %DateTime{} = last_notification_sent, _) do - is_delay_ok_since_last_notification_sent(last_notification_sent) + is_delay_ok_since_last_notification_sent?(last_notification_sent) end # This is a recap diff --git a/lib/service/notifier/push.ex b/lib/service/notifier/push.ex index 36a0cfc9..4fc7caac 100644 --- a/lib/service/notifier/push.ex +++ b/lib/service/notifier/push.ex @@ -66,12 +66,14 @@ defmodule Mobilizon.Service.Notifier.Push do Map.get(@default_behavior, activity_setting, false) end + @spec send_subscription(Activity.t(), any, Keyword.t()) :: no_return defp send_subscription(activity, subscription, options) do activity |> payload(options) |> WebPushEncryption.send_web_push(subscription) end + @spec payload(Activity.t(), Keyword.t()) :: String.t() defp payload(%Activity{} = activity, options) do activity |> Utils.add_activity_object() diff --git a/lib/service/rich_media/parser.ex b/lib/service/rich_media/parser.ex index 4556d829..0a6e3691 100644 --- a/lib/service/rich_media/parser.ex +++ b/lib/service/rich_media/parser.ex @@ -192,7 +192,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do end end - @spec maybe_parse(String.t()) :: {:halt, map()} | {:cont, map()} + @spec maybe_parse(String.t()) :: map() defp maybe_parse(html) do Enum.reduce_while(parsers(), %{}, fn parser, acc -> case parser.parse(html, acc) do @@ -286,7 +286,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do end end - @spec check_remote_picture_path(map()) :: map() + @spec check_remote_picture_path(map()) :: {:ok, map()} defp check_remote_picture_path(%{image_remote_url: image_remote_url, url: url} = data) when is_binary(image_remote_url) and is_binary(url) do Logger.debug("Checking image_remote_url #{image_remote_url}") diff --git a/lib/service/rich_media/parsers/meta_tags_parser.ex b/lib/service/rich_media/parsers/meta_tags_parser.ex index 768baffa..39f81732 100644 --- a/lib/service/rich_media/parsers/meta_tags_parser.ex +++ b/lib/service/rich_media/parsers/meta_tags_parser.ex @@ -73,9 +73,6 @@ defmodule Mobilizon.Service.RichMedia.Parsers.MetaTagsParser do "" -> meta - descriptions when is_list(descriptions) and length(descriptions) > 0 -> - Map.put_new(meta, :description, hd(descriptions)) - description -> Map.put_new(meta, :description, description) end @@ -99,8 +96,8 @@ defmodule Mobilizon.Service.RichMedia.Parsers.MetaTagsParser do with {:ok, document} <- Floki.parse_document(html), elem when not is_nil(elem) <- document |> Floki.find("html head meta[name='description']") |> List.first(), - description when is_binary(description) <- Floki.attribute(elem, "content") do - description + [_ | _] = descriptions <- Floki.attribute(elem, "content") do + hd(descriptions) else _ -> "" end diff --git a/lib/service/workers/activity_builder.ex b/lib/service/workers/activity_builder.ex index 2d9a49c3..0dcc2a23 100644 --- a/lib/service/workers/activity_builder.ex +++ b/lib/service/workers/activity_builder.ex @@ -12,15 +12,22 @@ defmodule Mobilizon.Service.Workers.ActivityBuilder do use Mobilizon.Service.Workers.Helper, queue: "activity" @impl Oban.Worker + @spec perform(Job.t()) :: {:ok, Activity.t()} | {:error, Ecto.Changeset.t()} def perform(%Job{args: args}) do - with {"build_activity", args} <- Map.pop(args, "op"), - {:ok, %Activity{} = activity} <- build_activity(args), - preloaded_activity <- Activities.preload_activity(activity) do - notify_activity(preloaded_activity) + {"build_activity", args} = Map.pop(args, "op") + + case build_activity(args) do + {:ok, %Activity{} = activity} -> + activity + |> Activities.preload_activity() + |> notify_activity() + + {:error, %Ecto.Changeset{} = err} -> + {:error, err} end end - @spec build_activity(map()) :: {:ok, Activity.t()} + @spec build_activity(map()) :: {:ok, Activity.t()} | {:error, Ecto.Changeset.t()} def build_activity(args) do Activities.create_activity(args) end diff --git a/lib/service/workers/background.ex b/lib/service/workers/background.ex index 07eb9c91..6f5175b9 100644 --- a/lib/service/workers/background.ex +++ b/lib/service/workers/background.ex @@ -11,11 +11,23 @@ defmodule Mobilizon.Service.Workers.Background do use Mobilizon.Service.Workers.Helper, queue: "background" @impl Oban.Worker + @spec perform(Job.t()) :: + {:ok, Actor.t()} + | {:error, + :actor_not_found | :bad_option_value_for_reserve_username | Ecto.Changeset.t()} def perform(%Job{args: %{"op" => "delete_actor", "actor_id" => actor_id} = args}) do - with reserve_username when is_boolean(reserve_username) <- - Map.get(args, "reserve_username", true), - %Actor{} = actor <- Actors.get_actor(actor_id) do - ActorSuspension.suspend_actor(actor, reserve_username: reserve_username) + case Map.get(args, "reserve_username", true) do + reserve_username when is_boolean(reserve_username) -> + case Actors.get_actor(actor_id) do + %Actor{} = actor -> + ActorSuspension.suspend_actor(actor, reserve_username: reserve_username) + + nil -> + {:error, :actor_not_found} + end + + _ -> + {:error, :bad_option_value_for_reserve_username} end end diff --git a/lib/service/workers/clean_orphan_media_worker.ex b/lib/service/workers/clean_orphan_media_worker.ex index 566120b2..c56c4a1f 100644 --- a/lib/service/workers/clean_orphan_media_worker.ex +++ b/lib/service/workers/clean_orphan_media_worker.ex @@ -4,11 +4,12 @@ defmodule Mobilizon.Service.Workers.CleanOrphanMediaWorker do """ use Oban.Worker, queue: "background" + alias Mobilizon.Config alias Mobilizon.Service.CleanOrphanMedia @impl Oban.Worker def perform(%Job{}) do - if Mobilizon.Config.get!([:instance, :remove_orphan_uploads]) and should_perform?() do + if Keyword.get(Config.instance_config(), :remove_orphan_uploads, false) and should_perform?() do CleanOrphanMedia.clean() end end @@ -17,8 +18,7 @@ defmodule Mobilizon.Service.Workers.CleanOrphanMediaWorker do defp should_perform? do case Cachex.get(:key_value, "last_media_cleanup") do {:ok, %DateTime{} = last_media_cleanup} -> - default_grace_period = - Mobilizon.Config.get([:instance, :orphan_upload_grace_period_hours], 48) + default_grace_period = Config.get([:instance, :orphan_upload_grace_period_hours], 48) DateTime.compare( last_media_cleanup, diff --git a/lib/service/workers/clean_suspended_actors.ex b/lib/service/workers/clean_suspended_actors.ex index 277f0f1b..44b74a4d 100644 --- a/lib/service/workers/clean_suspended_actors.ex +++ b/lib/service/workers/clean_suspended_actors.ex @@ -1,4 +1,4 @@ -defmodule Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker do +defmodule Mobilizon.Service.Workers.CleanSuspendedActors do @moduledoc """ Worker to clean unattached media """ diff --git a/lib/service/workers/helper.ex b/lib/service/workers/helper.ex index 53232563..28bb0d7b 100644 --- a/lib/service/workers/helper.ex +++ b/lib/service/workers/helper.ex @@ -11,6 +11,7 @@ defmodule Mobilizon.Service.Workers.Helper do alias Mobilizon.Config alias Mobilizon.Service.Workers.Helper + @spec worker_args(atom()) :: Keyword.t() def worker_args(queue) do case Config.get([:workers, :retries, queue]) do nil -> [] @@ -18,6 +19,7 @@ defmodule Mobilizon.Service.Workers.Helper do end end + @spec sidekiq_backoff(integer, integer, integer) :: integer def sidekiq_backoff(attempt, pow \\ 4, base_backoff \\ 15) do backoff = :math.pow(attempt, pow) + @@ -39,6 +41,8 @@ defmodule Mobilizon.Service.Workers.Helper do alias Oban.Job + @spec enqueue(String.t(), map(), Keyword.t()) :: + {:ok, Job.t()} | {:error, Ecto.Changeset.t()} def enqueue(operation, params, worker_args \\ []) do params = Map.merge(%{"op" => operation}, params) queue_atom = String.to_existing_atom(unquote(queue)) diff --git a/lib/service/workers/legacy_notifier_builder.ex b/lib/service/workers/legacy_notifier_builder.ex index 9a6ef139..86b8bafc 100644 --- a/lib/service/workers/legacy_notifier_builder.ex +++ b/lib/service/workers/legacy_notifier_builder.ex @@ -13,17 +13,16 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do @impl Oban.Worker def perform(%Job{args: args}) do - with {"legacy_notify", args} <- Map.pop(args, "op") do - activity = build_activity(args) + {"legacy_notify", args} = Map.pop(args, "op") + activity = build_activity(args) - if args["subject"] == "participation_event_comment" do - notify_anonymous_participants(get_in(args, ["subject_params", "event_id"]), activity) - end - - args - |> users_to_notify(author_id: args["author_id"], group_id: Map.get(args, "group_id")) - |> Enum.each(&Notifier.notify(&1, activity, single_activity: true)) + if args["subject"] == "participation_event_comment" do + notify_anonymous_participants(get_in(args, ["subject_params", "event_id"]), activity) end + + args + |> users_to_notify(author_id: args["author_id"], group_id: Map.get(args, "group_id")) + |> Enum.each(&Notifier.notify(&1, activity, single_activity: true)) end defp build_activity(args) do @@ -105,8 +104,8 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do is_map(metadata) && is_binary(metadata.email) end) |> Enum.map(fn %Participant{metadata: metadata} -> metadata end) - |> Enum.map(fn metadata -> - Notifier.Email.send_anonymous_activity(metadata.email, activity, + |> Enum.map(fn %{email: email} = metadata -> + Notifier.Email.send_anonymous_activity(email, activity, locale: Map.get(metadata, :locale, "en") ) end) diff --git a/lib/service/workers/send_activity_recap_worker.ex b/lib/service/workers/send_activity_recap_worker.ex index c1a52a26..8b87cbb9 100644 --- a/lib/service/workers/send_activity_recap_worker.ex +++ b/lib/service/workers/send_activity_recap_worker.ex @@ -13,9 +13,9 @@ defmodule Mobilizon.Service.Workers.SendActivityRecapWorker do import Mobilizon.Service.DateTime, only: [ - is_between_hours: 1, - is_between_hours_on_first_day: 1, - is_delay_ok_since_last_notification_sent: 1 + is_between_hours?: 1, + is_between_hours_on_first_day?: 1, + is_delay_ok_since_last_notification_sent?: 1 ] @impl Oban.Worker @@ -86,7 +86,7 @@ defmodule Mobilizon.Service.Workers.SendActivityRecapWorker do group_notifications: :one_hour } }) do - is_delay_ok_since_last_notification_sent(last_notification_sent) + is_delay_ok_since_last_notification_sent?(last_notification_sent) end # If we're between notification hours @@ -96,7 +96,7 @@ defmodule Mobilizon.Service.Workers.SendActivityRecapWorker do timezone: timezone } }) do - is_between_hours(timezone: timezone || "Etc/UTC") + is_between_hours?(timezone: timezone || "Etc/UTC") end # If we're on the first day of the week between notification hours @@ -107,6 +107,6 @@ defmodule Mobilizon.Service.Workers.SendActivityRecapWorker do timezone: timezone } }) do - is_between_hours_on_first_day(timezone: timezone || "Etc/UTC", locale: locale) + is_between_hours_on_first_day?(timezone: timezone || "Etc/UTC", locale: locale) end end diff --git a/lib/web/auth/context.ex b/lib/web/auth/context.ex index 9e96dac2..dbf216c9 100644 --- a/lib/web/auth/context.ex +++ b/lib/web/auth/context.ex @@ -25,7 +25,7 @@ defmodule Mobilizon.Web.Auth.Context do defp set_user_information_in_context(conn) do context = %{ip: conn.remote_ip |> :inet.ntoa() |> to_string()} - user_agent = Plug.Conn.get_req_header(conn, "user-agent") |> List.first() + user_agent = conn |> Plug.Conn.get_req_header("user-agent") |> List.first() {conn, context} = case Guardian.Plug.current_resource(conn) do @@ -41,7 +41,7 @@ defmodule Mobilizon.Web.Auth.Context do }, query_string: conn.query_string, env: %{ - REQUEST_ID: Plug.Conn.get_resp_header(conn, "x-request-id") |> List.first(), + REQUEST_ID: conn |> Plug.Conn.get_resp_header("x-request-id") |> List.first(), SERVER_NAME: conn.host } }) diff --git a/lib/web/auth/guardian.ex b/lib/web/auth/guardian.ex index 8797dd06..da1f23c0 100644 --- a/lib/web/auth/guardian.ex +++ b/lib/web/auth/guardian.ex @@ -23,6 +23,8 @@ defmodule Mobilizon.Web.Auth.Guardian do {:error, :unknown_resource} end + @spec resource_from_claims(any) :: + {:error, :invalid_id | :no_result | :no_claims} | {:ok, Mobilizon.Users.User.t()} def resource_from_claims(%{"sub" => "User:" <> uid_str}) do Logger.debug(fn -> "Receiving claim for user #{uid_str}" end) @@ -40,7 +42,7 @@ defmodule Mobilizon.Web.Auth.Guardian do end def resource_from_claims(_) do - {:error, :reason_for_error} + {:error, :no_claims} end def after_encode_and_sign(resource, claims, token, _options) do diff --git a/lib/web/cache/activity_pub.ex b/lib/web/cache/activity_pub.ex index 97619121..d19b70a0 100644 --- a/lib/web/cache/activity_pub.ex +++ b/lib/web/cache/activity_pub.ex @@ -23,15 +23,18 @@ defmodule Mobilizon.Web.Cache.ActivityPub do @spec get_actor_by_name(String.t()) :: {:commit, ActorModel.t()} | {:ignore, nil} def get_actor_by_name(name) do - Cachex.fetch(@cache, "actor_" <> name, fn "actor_" <> name -> - case Actor.find_or_make_actor_from_nickname(name) do - {:ok, %ActorModel{} = actor} -> - {:commit, actor} + Cachex.fetch(@cache, "actor_" <> name, &do_get_actor/1) + end - nil -> - {:ignore, nil} - end - end) + @spec do_get_actor(String.t()) :: {:commit, Actor.t()} | {:ignore, nil} + defp do_get_actor("actor_" <> name) do + case Actor.find_or_make_actor_from_nickname(name) do + {:ok, %ActorModel{} = actor} -> + {:commit, actor} + + {:error, _err} -> + {:ignore, nil} + end end @doc """ @@ -179,7 +182,7 @@ defmodule Mobilizon.Web.Cache.ActivityPub do Gets a member by its UUID, with all associations loaded. """ @spec get_member_by_uuid_with_preload(String.t()) :: - {:commit, Todo.t()} | {:ignore, nil} + {:commit, Member.t()} | {:ignore, nil} def get_member_by_uuid_with_preload(uuid) do Cachex.fetch(@cache, "member_" <> uuid, fn "member_" <> uuid -> case Actors.get_member(uuid) do diff --git a/lib/web/cache/cache.ex b/lib/web/cache/cache.ex index 08e3068f..d90007a0 100644 --- a/lib/web/cache/cache.ex +++ b/lib/web/cache/cache.ex @@ -3,7 +3,12 @@ defmodule Mobilizon.Web.Cache do Facade module which provides access to all cached data. """ - alias Mobilizon.Actors.Actor + alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Discussions.{Comment, Discussion} + alias Mobilizon.Events.Event + alias Mobilizon.Posts.Post + alias Mobilizon.Resources.Resource + alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Web.Cache.ActivityPub import Mobilizon.Service.Guards, only: [is_valid_string: 1] @@ -20,15 +25,26 @@ defmodule Mobilizon.Web.Cache do Enum.each(@caches, &Cachex.del(&1, "actor_" <> preferred_username)) end + @spec get_actor_by_name(binary) :: {:commit, Actor.t()} | {:ignore, nil} defdelegate get_actor_by_name(name), to: ActivityPub + @spec get_local_actor_by_name(binary) :: {:commit, Actor.t()} | {:ignore, nil} defdelegate get_local_actor_by_name(name), to: ActivityPub + @spec get_public_event_by_uuid_with_preload(binary) :: {:commit, Event.t()} | {:ignore, nil} defdelegate get_public_event_by_uuid_with_preload(uuid), to: ActivityPub + @spec get_comment_by_uuid_with_preload(binary) :: {:commit, Comment.t()} | {:ignore, nil} defdelegate get_comment_by_uuid_with_preload(uuid), to: ActivityPub + @spec get_resource_by_uuid_with_preload(binary) :: {:commit, Resource.t()} | {:ignore, nil} defdelegate get_resource_by_uuid_with_preload(uuid), to: ActivityPub + @spec get_todo_list_by_uuid_with_preload(binary) :: {:commit, TodoList.t()} | {:ignore, nil} defdelegate get_todo_list_by_uuid_with_preload(uuid), to: ActivityPub + @spec get_todo_by_uuid_with_preload(binary) :: {:commit, Todo.t()} | {:ignore, nil} defdelegate get_todo_by_uuid_with_preload(uuid), to: ActivityPub + @spec get_member_by_uuid_with_preload(binary) :: {:commit, Member.t()} | {:ignore, nil} defdelegate get_member_by_uuid_with_preload(uuid), to: ActivityPub + @spec get_post_by_slug_with_preload(binary) :: {:commit, Post.t()} | {:ignore, nil} defdelegate get_post_by_slug_with_preload(slug), to: ActivityPub + @spec get_discussion_by_slug_with_preload(binary) :: {:commit, Discussion.t()} | {:ignore, nil} defdelegate get_discussion_by_slug_with_preload(slug), to: ActivityPub + @spec get_relay :: {:commit, Actor.t()} | {:ignore, nil} defdelegate get_relay, to: ActivityPub end diff --git a/lib/web/controllers/activity_pub_controller.ex b/lib/web/controllers/activity_pub_controller.ex index d8609359..296d774d 100644 --- a/lib/web/controllers/activity_pub_controller.ex +++ b/lib/web/controllers/activity_pub_controller.ex @@ -24,6 +24,7 @@ defmodule Mobilizon.Web.ActivityPubController do plug(Mobilizon.Web.Plugs.Federating when action in [:inbox, :relay]) plug(:relay_active? when action in [:relay]) + @spec relay_active?(Plug.Conn.t(), any()) :: Plug.Conn.t() def relay_active?(conn, _) do if Config.get([:instance, :allow_relay]) do conn @@ -35,34 +36,42 @@ defmodule Mobilizon.Web.ActivityPubController do end end + @spec following(Plug.Conn.t(), map()) :: Plug.Conn.t() def following(conn, args) do actor_collection(conn, "following", args) end + @spec followers(Plug.Conn.t(), map()) :: Plug.Conn.t() def followers(conn, args) do actor_collection(conn, "followers", args) end + @spec members(Plug.Conn.t(), map()) :: Plug.Conn.t() def members(conn, args) do actor_collection(conn, "members", args) end + @spec resources(Plug.Conn.t(), map()) :: Plug.Conn.t() def resources(conn, args) do actor_collection(conn, "resources", args) end + @spec posts(Plug.Conn.t(), map()) :: Plug.Conn.t() def posts(conn, args) do actor_collection(conn, "posts", args) end + @spec todos(Plug.Conn.t(), map()) :: Plug.Conn.t() def todos(conn, args) do actor_collection(conn, "todos", args) end + @spec events(Plug.Conn.t(), map()) :: Plug.Conn.t() def events(conn, args) do actor_collection(conn, "events", args) end + @spec discussions(Plug.Conn.t(), map()) :: Plug.Conn.t() def discussions(conn, args) do actor_collection(conn, "discussions", args) end @@ -70,38 +79,45 @@ defmodule Mobilizon.Web.ActivityPubController do @ok_statuses [:ok, :commit] @spec member(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t() def member(conn, %{"uuid" => uuid}) do - with {status, %Member{parent: %Actor{} = group, actor: %Actor{domain: nil} = _actor} = member} - when status in @ok_statuses <- - Cache.get_member_by_uuid_with_preload(uuid), - actor <- Map.get(conn.assigns, :actor), - true <- actor_applicant_group_member?(group, actor) do - json( - conn, - ActorView.render("member.json", %{ - member: member, - actor_applicant: actor - }) - ) - else + case Cache.get_member_by_uuid_with_preload(uuid) do + {status, %Member{parent: %Actor{} = group, actor: %Actor{domain: nil} = _actor} = member} + when status in @ok_statuses -> + actor = Map.get(conn.assigns, :actor) + + if actor_applicant_group_member?(group, actor) do + json( + conn, + ActorView.render("member.json", %{ + member: member, + actor_applicant: actor + }) + ) + else + not_found(conn) + end + {status, %Member{actor: %Actor{url: domain}, parent: %Actor{} = group, url: url}} when status in @ok_statuses and not is_nil(domain) -> - with actor <- Map.get(conn.assigns, :actor), - true <- actor_applicant_group_member?(group, actor) do + actor = Map.get(conn.assigns, :actor) + + if actor_applicant_group_member?(group, actor) do redirect(conn, external: url) else - _ -> - conn - |> put_status(404) - |> json("Not found") + not_found(conn) end _ -> - conn - |> put_status(404) - |> json("Not found") + not_found(conn) end end + @spec not_found(Plug.Conn.t()) :: Plug.Conn.t() + defp not_found(conn) do + conn + |> put_status(404) + |> json("Not found") + end + def outbox(conn, args) do actor_collection(conn, "outbox", args) end @@ -201,6 +217,7 @@ defmodule Mobilizon.Web.ActivityPubController do end end + @spec actor_applicant_group_member?(Actor.t(), Actor.t() | nil) :: boolean() defp actor_applicant_group_member?(%Actor{}, nil), do: false defp actor_applicant_group_member?(%Actor{id: group_id}, %Actor{id: actor_applicant_id}), diff --git a/lib/web/email/actor.ex b/lib/web/email/actor.ex new file mode 100644 index 00000000..8e81621c --- /dev/null +++ b/lib/web/email/actor.ex @@ -0,0 +1,60 @@ +defmodule Mobilizon.Web.Email.Actor do + @moduledoc """ + Handles emails sent about actors status. + """ + use Bamboo.Phoenix, view: Mobilizon.Web.EmailView + + import Bamboo.Phoenix + import Mobilizon.Web.Gettext + + alias Mobilizon.Actors.Actor + alias Mobilizon.{Config, Users} + alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Users.User + alias Mobilizon.Web.Email + + @doc """ + Send a notification to participants from events organized by an actor that is going to be suspended + """ + @spec send_notification_event_participants_from_suspension(Participant.t(), Actor.t()) :: + :ok + def send_notification_event_participants_from_suspension( + %Participant{ + actor: %Actor{user_id: nil} + }, + _suspended + ), + do: :ok + + def send_notification_event_participants_from_suspension(%Participant{role: role}, _suspended) + when role not in [:participant, :moderator, :administrator], + do: :ok + + def send_notification_event_participants_from_suspension( + %Participant{ + actor: %Actor{user_id: user_id}, + event: %Event{} = event, + role: member_role + }, + %Actor{} = suspended + ) do + with %User{email: email, locale: locale} <- Users.get_user!(user_id) do + Gettext.put_locale(locale) + instance = Config.instance_name() + + subject = gettext("Your participation to %{event} has been cancelled!", event: event.title) + + Email.base_email(to: email, subject: subject) + |> assign(:locale, locale) + |> assign(:actor, suspended) + |> assign(:event, event) + |> assign(:role, member_role) + |> assign(:subject, subject) + |> assign(:instance, instance) + |> render(:actor_suspension_participants) + |> Email.Mailer.send_email_later() + + :ok + end + end +end diff --git a/lib/web/email/group.ex b/lib/web/email/group.ex index ea4a0ee0..bf1ecdc1 100644 --- a/lib/web/email/group.ex +++ b/lib/web/email/group.ex @@ -79,6 +79,7 @@ defmodule Mobilizon.Web.Email.Group do # TODO : def send_confirmation_to_inviter() @member_roles [:administrator, :moderator, :member] + @spec send_group_suspension_notification(Member.t()) :: :ok def send_group_suspension_notification(%Member{actor: %Actor{user_id: nil}}), do: :ok def send_group_suspension_notification(%Member{role: role}) when role not in @member_roles, @@ -112,64 +113,4 @@ defmodule Mobilizon.Web.Email.Group do :ok end end - - def send_group_deletion_notification(%Member{actor: %Actor{user_id: nil}}, _author), do: :ok - - def send_group_deletion_notification(%Member{role: role}, _author) - when role not in @member_roles, - do: :ok - - @spec send_group_deletion_notification(Member.t(), Actor.t()) :: :ok - def send_group_deletion_notification( - %Member{ - actor: %Actor{user_id: user_id, id: actor_id} = member - }, - %Actor{id: author_id} = author - ) do - with %User{email: email, locale: locale} <- Users.get_user!(user_id), - {:member_not_author, true} <- {:member_not_author, author_id !== actor_id} do - do_send_group_deletion_notification(member, author: author, email: email, locale: locale) - else - # Skip if it's the author itself - {:member_not_author, _} -> - :ok - end - end - - @spec send_group_deletion_notification(Member.t()) :: :ok - def send_group_deletion_notification(%Member{actor: %Actor{user_id: user_id}} = member) do - case Users.get_user!(user_id) do - %User{email: email, locale: locale} -> - do_send_group_deletion_notification(member, email: email, locale: locale) - end - end - - defp do_send_group_deletion_notification( - %Member{role: member_role, parent: %Actor{domain: nil} = group}, - options - ) do - locale = Keyword.get(options, :locale) - Gettext.put_locale(locale) - instance = Config.instance_name() - author = Keyword.get(options, :author) - - subject = - gettext( - "The group %{group} has been deleted on %{instance}", - group: group.name, - instance: instance - ) - - Email.base_email(to: Keyword.get(options, :email), subject: subject) - |> assign(:locale, locale) - |> assign(:group, group) - |> assign(:role, member_role) - |> assign(:subject, subject) - |> assign(:instance, instance) - |> assign(:author, author) - |> render(:group_deletion) - |> Email.Mailer.send_email_later() - - :ok - end end diff --git a/lib/web/email/user.ex b/lib/web/email/user.ex index 638f0dcc..29887c35 100644 --- a/lib/web/email/user.ex +++ b/lib/web/email/user.ex @@ -199,14 +199,14 @@ defmodule Mobilizon.Web.Email.User do :ok _ -> - case Timex.before?( - Timex.shift(Map.get(user, key), hours: 1), + case DateTime.compare( + DateTime.add(Map.get(user, key), 3600), DateTime.utc_now() |> DateTime.truncate(:second) ) do - true -> + :lt -> :ok - false -> + _ -> {:error, :email_too_soon} end end diff --git a/lib/web/plugs/http_security_plug.ex b/lib/web/plugs/http_security_plug.ex index fd2ddba6..8a278074 100644 --- a/lib/web/plugs/http_security_plug.ex +++ b/lib/web/plugs/http_security_plug.ex @@ -117,7 +117,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do |> to_string() end - @spec add_csp_param(list(), list(String.t()) | String.t() | nil) :: list() + @spec add_csp_param(iodata(), iodata() | nil) :: list() defp add_csp_param(csp_iodata, nil), do: csp_iodata defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] @@ -132,7 +132,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do defp maybe_send_sts_header(conn, false), do: conn - @spec get_csp_config(atom(), Keyword.t()) :: String.t() + @spec get_csp_config(atom(), Keyword.t()) :: iodata() defp get_csp_config(type, options) do options |> Keyword.get(type, Config.get([:http_security, :csp_policy, type])) diff --git a/lib/web/plugs/http_signatures.ex b/lib/web/plugs/http_signatures.ex index f57b4d4c..595b3b47 100644 --- a/lib/web/plugs/http_signatures.ex +++ b/lib/web/plugs/http_signatures.ex @@ -8,7 +8,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSignatures do Plug to check HTTP Signatures on every incoming request """ - import Plug.Conn + import Plug.Conn, only: [get_req_header: 2, put_req_header: 3, assign: 3] require Logger @@ -22,48 +22,61 @@ defmodule Mobilizon.Web.Plugs.HTTPSignatures do end def call(conn, _opts) do - case get_req_header(conn, "signature") do - [signature | _] -> - if signature do - # set (request-target) header to the appropriate value - # we also replace the digest header with the one we computed - conn = - conn - |> put_req_header( - "(request-target)", - String.downcase("#{conn.method}") <> " #{conn.request_path}" - ) + signature = conn |> get_req_header("signature") |> List.first() - conn = - if conn.assigns[:digest] do - conn - |> put_req_header("digest", conn.assigns[:digest]) - else - conn - end + if is_nil(signature) do + Logger.debug("No signature header!") + conn + else + # set (request-target) header to the appropriate value + # we also replace the digest header with the one we computed + conn = + conn + |> put_req_header( + "(request-target)", + String.downcase("#{conn.method}") <> " #{conn.request_path}" + ) - signature_valid = HTTPSignatures.validate_conn(conn) - Logger.debug("Is signature valid ? #{inspect(signature_valid)}") - date_valid = date_valid?(conn) - assign(conn, :valid_signature, signature_valid && date_valid) + conn = + if conn.assigns[:digest] do + conn + |> put_req_header("digest", conn.assigns[:digest]) else - Logger.debug("No signature header!") conn end - _ -> - conn + signature_valid = HTTPSignatures.validate_conn(conn) + Logger.debug("Is signature valid ? #{inspect(signature_valid)}") + date_valid = date_valid?(conn) + assign(conn, :valid_signature, signature_valid && date_valid) end end @spec date_valid?(Plug.Conn.t()) :: boolean() defp date_valid?(conn) do - with [date | _] <- get_req_header(conn, "date") || [""], - {:ok, date} <- Timex.parse(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") do - Timex.diff(date, DateTime.utc_now(), :hours) <= 12 && - Timex.diff(date, DateTime.utc_now(), :hours) >= -12 + date = conn |> get_req_header("date") |> List.first() + + if is_nil(date) do + false else - _ -> false + case Timex.parse(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") do + {:ok, %NaiveDateTime{} = date} -> + date + |> DateTime.from_naive!("Etc/UTC") + |> date_diff_ok?() + + {:ok, %DateTime{} = date} -> + date_diff_ok?(date) + + {:error, _err} -> + false + end end end + + @spec date_diff_ok?(DateTime.t()) :: boolean() + defp date_diff_ok?(%DateTime{} = date) do + DateTime.diff(date, DateTime.utc_now()) <= 12 * 3600 && + DateTime.diff(date, DateTime.utc_now()) >= -12 * 3600 + end end diff --git a/lib/web/templates/email/group_deletion.html.eex b/lib/web/templates/email/actor_suspension_participants.html.eex similarity index 77% rename from lib/web/templates/email/group_deletion.html.eex rename to lib/web/templates/email/actor_suspension_participants.html.eex index da88eff3..10c150ae 100644 --- a/lib/web/templates/email/group_deletion.html.eex +++ b/lib/web/templates/email/actor_suspension_participants.html.eex @@ -10,7 +10,7 @@
- <%= gettext "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted.", - author: Mobilizon.Actors.Actor.display_name_and_username(@author), - group: Mobilizon.Actors.Actor.display_name_and_username(@group) %> + <%= gettext "Your instance's moderation team has decided to suspend %{actor_name} (%{actor_address}). All of their events have been removed and your participation cancelled.", group_name: @actor.name || @actor.preferred_username, actor_address: if @actor.domain, do: "@#{@actor.preferred_username}@#{@actor.domain}", else: "@#{@actor.preferred_username}" %>