mobilizon/lib/graphql/resolvers/participant.ex
Thomas Citharel b5672cee7e
WIP
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2023-11-14 14:24:17 +01:00

435 lines
15 KiB
Elixir

defmodule Mobilizon.GraphQL.Resolvers.Participant do
@moduledoc """
Handles the participation-related GraphQL calls.
"""
# alias Mobilizon.Conversations.ConversationParticipant
alias Mobilizon.{Actors, Config, Crypto, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Conversations.{Conversation, ConversationView}
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.GraphQL.API.Comments
alias Mobilizon.GraphQL.API.Participations
alias Mobilizon.Service.Export.Participants.{CSV, ODS, PDF}
alias Mobilizon.Users.User
alias Mobilizon.Web.Email
alias Mobilizon.Web.Email.Checker
require Logger
import Mobilizon.Web.Gettext
import Mobilizon.GraphQL.Resolvers.Event.Utils
@doc """
Join an event for an regular or anonymous actor
"""
@spec actor_join_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Participant.t()} | {:error, String.t()}
def actor_join_event(
_parent,
%{actor_id: actor_id, event_id: event_id} = args,
%{context: %{current_user: %User{} = user}}
) do
case User.owns_actor(user, actor_id) do
{:is_owned, %Actor{} = actor} ->
do_actor_join_event(actor, event_id, args)
_ ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
end
end
def actor_join_event(
_parent,
%{actor_id: actor_id, event_id: event_id} = args,
_resolution
) do
with {:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
{:anonymous_participation_enabled, true} <-
{:anonymous_participation_enabled,
event.local == true && Config.anonymous_participation?() &&
event.options.anonymous_participation == true},
{:anonymous_actor_id, true} <-
{:anonymous_actor_id, to_string(Config.anonymous_actor_id()) == actor_id},
{:email_required, true} <-
{:email_required,
Config.anonymous_participation_email_required?() &&
args |> Map.get(:email) |> valid_email?()},
{:confirmation_token, {confirmation_token, role}} <-
{:confirmation_token,
if(Config.anonymous_participation_email_confirmation_required?(),
do: {Crypto.random_string(30), :not_confirmed},
else: {nil, :participant}
)},
# We only federate if the participation is not to be confirmed later
args <-
args
|> Map.put(:confirmation_token, confirmation_token)
|> Map.put(:cancellation_token, Crypto.random_string(30))
|> Map.put(:role, role)
|> Map.put(:local, role == :participant),
{:actor_not_found, %Actor{} = actor} <-
{:actor_not_found, Actors.get_actor_with_preload(actor_id)},
{:ok, %Participant{} = participant} <- do_actor_join_event(actor, event_id, args) do
if Config.anonymous_participation_email_required?() &&
Config.anonymous_participation_email_confirmation_required?() do
args
|> Map.get(:email)
|> Email.Participation.anonymous_participation_confirmation(
participant,
Map.get(args, :locale, "en")
)
|> Email.Mailer.send_email()
end
{:ok, participant}
else
{:error, err} ->
{:error, err}
{:has_event, _} ->
{:error,
dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))}
{:anonymous_participation_enabled, false} ->
{:error, dgettext("errors", "Anonymous participation is not enabled")}
{:anonymous_actor_id, false} ->
{:error, dgettext("errors", "Profile ID provided is not the anonymous profile one")}
{:email_required, _} ->
{:error, dgettext("errors", "A valid email is required by your instance")}
{:actor_not_found, _} ->
Logger.error(
"The actor ID \"#{actor_id}\" provided by configuration doesn't match any actor in database"
)
{:error, dgettext("errors", "Internal Error")}
end
end
def actor_join_event(_parent, _args, _resolution) do
{:error, dgettext("errors", "You need to be logged-in to join an event")}
end
@spec do_actor_join_event(Actor.t(), integer | String.t(), map()) ::
{:ok, Participant.t()} | {:error, String.t()}
defp do_actor_join_event(actor, event_id, args) do
with {:has_event, {:ok, %Event{} = event}} <-
{:has_event, Events.get_event_with_preload(event_id)},
{:ok, _activity, participant} <- Participations.join(event, actor, args),
%Participant{} = participant <-
participant
|> Map.put(:event, event)
|> Map.put(:actor, actor) do
{:ok, participant}
else
{:error, :maximum_attendee_capacity_reached} ->
{:error, dgettext("errors", "The event has already reached its maximum capacity")}
{:has_event, _} ->
{:error,
dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))}
{:error, :event_not_found} ->
{:error, dgettext("errors", "Event id not found")}
{: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
"""
@spec actor_leave_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, map()} | {:error, String.t()}
def actor_leave_event(
_parent,
%{actor_id: actor_id, event_id: event_id, token: token},
_resolution
)
when not is_nil(token) do
case check_anonymous_participation(actor_id, event_id) do
{:ok, %Event{} = event} ->
%Actor{} = actor = Actors.get_actor_with_preload!(actor_id)
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}}
{: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, _err} ->
{:error, dgettext("errors", "Failed to leave the event")}
end
end
end
def actor_leave_event(
_parent,
%{actor_id: actor_id, event_id: event_id},
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Events.get_event_with_preload(event_id)},
{:ok, _activity, _participant} <- Participations.leave(event, actor) do
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, nil} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
{: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")}
end
end
def actor_leave_event(_parent, _args, _resolution) do
{:error, dgettext("errors", "You need to be logged-in to leave an event")}
end
@spec update_participation(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Participation.t()} | {:error, String.t() | Ecto.Changeset.t()}
def update_participation(
_parent,
%{id: participation_id, role: new_role},
%{
context: %{
current_actor: %Actor{} = moderator_actor
}
}
) do
# Check that participation already exists
case Events.get_participant(participation_id) do
%Participant{role: old_role, event_id: event_id} = participation ->
if new_role != old_role do
%Event{} = event = Events.get_event_with_preload!(event_id)
if can_event_be_updated_by?(event, moderator_actor) do
with {:ok, _activity, participation} <-
Participations.update(participation, moderator_actor, new_role) do
{:ok, participation}
end
else
{:error,
dgettext(
"errors",
"Provided profile doesn't have moderator permissions on this event"
)}
end
else
{:error, dgettext("errors", "Participant already has role %{role}", role: new_role)}
end
nil ->
{:error, dgettext("errors", "Participant not found")}
end
end
@spec confirm_participation_from_token(map(), map(), map()) ::
{:ok, Participant.t()} | {:error, String.t()}
def confirm_participation_from_token(
_parent,
%{confirmation_token: confirmation_token},
_context
) do
with {:has_participant,
%Participant{actor: actor, role: :not_confirmed, event: event} = participant} <-
{:has_participant, Events.get_participant_by_confirmation_token(confirmation_token)},
{:ok, _activity, %Participant{} = participant} <-
Participations.update(participant, actor, Events.get_default_participant_role(event)) do
{:ok, participant}
else
{:has_participant, %Participant{role: :not_approved}} ->
{:error,
dgettext("errors", "Participation is confirmed but not approved yet by an organizer")}
{:has_participant, %Participant{role: :participant}} ->
{:error, dgettext("errors", "Participation is already confirmed")}
{:has_participant, nil} ->
{:error, dgettext("errors", "This token is invalid")}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end
end
@spec export_event_participants(any(), map(), Absinthe.Resolution.t()) :: {:ok, String.t()}
def export_event_participants(_parent, %{event_id: event_id, roles: roles, format: format}, %{
context: %{
current_user: %User{locale: locale},
current_actor: %Actor{} = moderator_actor
}
}) do
case Events.get_event_with_preload(event_id) do
{:ok, %Event{} = event} ->
if can_event_be_updated_by?(event, moderator_actor) do
case export_format(format, event, roles, locale) do
{:ok, path} ->
{:ok, %{path: path, format: format}}
{:error, :export_dependency_not_installed} ->
{:error,
dgettext(
"errors",
"A dependency needed to export to %{format} is not installed",
format: format
)}
{:error, :failed_to_save_upload} ->
{:error,
dgettext(
"errors",
"An error occured while saving export",
format: format
)}
{:error, :format_not_supported} ->
{:error,
dgettext(
"errors",
"Format not supported"
)}
end
else
{:error,
dgettext(
"errors",
"Provided profile doesn't have moderator permissions on this event"
)}
end
{:error, :event_not_found} ->
{:error,
dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))}
end
end
def export_event_participants(_, _, _), do: {:error, :unauthorized}
def send_private_messages_to_participants(
_parent,
%{roles: roles, event_id: event_id, actor_id: actor_id} =
args,
%{
context: %{
current_user: %User{locale: _locale},
current_actor: %Actor{id: current_actor_id}
}
}
) do
participant_actors =
event_id
|> Events.list_all_participants_for_event(roles)
|> Enum.map(& &1.actor)
mentions =
participant_actors
|> Enum.map(& &1.id)
|> Enum.uniq()
|> Enum.map(&%{actor_id: &1, event_id: event_id})
args =
Map.merge(args, %{
mentions: mentions,
visibility: :private
})
with {:member, true} <-
{:member,
current_actor_id == actor_id or Actors.is_member?(current_actor_id, actor_id)},
{:ok, _activity, %Conversation{} = conversation} <- Comments.create_conversation(args) do
{:ok, conversation_to_view(conversation, Actors.get_actor(actor_id))}
else
{:member, false} ->
{:error, :unauthorized}
{:error, err} ->
{:error, err}
end
end
def send_private_messages_to_participants(_parent, _args, _resolution),
do: {:error, :unauthorized}
defp conversation_to_view(%Conversation{} = conversation, %Actor{} = actor) do
value =
conversation
|> Map.from_struct()
|> Map.put(:actor, actor)
struct(ConversationView, value)
end
@spec valid_email?(String.t() | nil) :: boolean
defp valid_email?(email) when is_nil(email), do: false
defp valid_email?(email) when is_binary(email) do
email
|> String.trim()
|> Checker.valid?()
end
@spec export_format(atom(), Event.t(), list(), String.t()) ::
{:ok, String.t()}
| {:error,
:format_not_supported | :export_dependency_not_installed | :failed_to_save_upload}
defp export_format(format, event, roles, locale) do
case format do
:csv ->
CSV.export(event, roles: roles, locale: locale)
:pdf ->
PDF.export(event, roles: roles, locale: locale)
:ods ->
ODS.export(event, roles: roles, locale: locale)
_ ->
{:error, :format_not_supported}
end
end
end