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