diff --git a/lib/mobilizon/actors/user.ex b/lib/mobilizon/actors/user.ex index d69be537b..da36a3540 100644 --- a/lib/mobilizon/actors/user.ex +++ b/lib/mobilizon/actors/user.ex @@ -138,15 +138,10 @@ defmodule Mobilizon.Actors.User do {:ok, user} end - def owns_actor(%User{default_actor_id: default_actor_id}, %Actor{id: actor_id}) - when default_actor_id == actor_id do - {:is_owned, true} - end - def owns_actor(%User{actors: actors}, actor_id) do - case Enum.any?(actors, fn a -> a.id == actor_id end) do - true -> {:is_owned, true} - _ -> {:is_owned, false} + case Enum.find(actors, fn a -> a.id == actor_id end) do + nil -> {:is_owned, false} + actor -> {:is_owned, true, actor} end end end diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 6cfb79c3c..e0c9bc137 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -582,6 +582,28 @@ defmodule Mobilizon.Events do ) end + @doc """ + Returns the list of organizers participants for an event. + + ## Examples + + iex> list_organizers_participants_for_event(id) + [%Participant{role: :creator}, ...] + + """ + def list_organizers_participants_for_event(id, page \\ nil, limit \\ nil) do + Repo.all( + from( + p in Participant, + join: e in Event, + on: p.event_id == e.id, + where: e.id == ^id and p.role == ^:creator, + preload: [:actor] + ) + |> paginate(page, limit) + ) + end + @doc """ Gets a single participant. @@ -600,6 +622,16 @@ defmodule Mobilizon.Events do Repo.get_by!(Participant, event_id: event_id, actor_id: actor_id) end + @doc """ + Get a single participant + """ + def get_participant(event_id, actor_id) do + case Repo.get_by(Participant, event_id: event_id, actor_id: actor_id) do + nil -> {:error, :participant_not_found} + participant -> {:ok, participant} + end + end + @doc """ Creates a participant. @@ -665,6 +697,18 @@ defmodule Mobilizon.Events do Participant.changeset(participant, %{}) end + @doc """ + Get the default participant role depending on the event join options + """ + def get_default_participant_role(%Event{} = event) do + case event.join_options do + # Participant + :free -> :participant + # Not approved + _ -> :not_approved + end + end + @doc """ List event participation requests for an actor """ diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index b11ba3831..74938c19d 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -4,7 +4,7 @@ defmodule MobilizonWeb.Resolvers.Event do """ alias Mobilizon.Service.ActivityPub alias Mobilizon.Activity - alias Mobilizon.Events.Event + alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Actors.User # We limit the max number of events that can be retrieved @@ -43,6 +43,85 @@ defmodule MobilizonWeb.Resolvers.Event do {:ok, Mobilizon.Events.list_participants_for_event(uuid, 1, 10)} end + @doc """ + Join an event for an actor + """ + def actor_join_event( + _parent, + %{actor_id: actor_id, event_id: event_id}, + %{ + context: %{ + current_user: user + } + } + ) do + with {:is_owned, true, actor} <- User.owns_actor(user, actor_id), + {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id), + {:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id), + role <- Mobilizon.Events.get_default_participant_role(event), + {:ok, participant} <- + Mobilizon.Events.create_participant(%{ + role: role, + event_id: event.id, + actor_id: actor.id + }), + participant <- + Map.put(participant, :event, event) + |> Map.put(:actor, actor) do + {:ok, participant} + else + {:is_owned, false} -> + {:error, "Actor id is not owned by authenticated user"} + + {:error, :event_not_found} -> + {:error, "Event id not found"} + + {:ok, %Participant{}} -> + {:error, "You are already a participant of this event"} + end + end + + def actor_join_event(_parent, _args, _resolution) do + {:error, "You need to be logged-in to join an event"} + end + + @doc """ + Leave an event for an actor + """ + def actor_leave_event( + _parent, + %{actor_id: actor_id, event_id: event_id}, + %{ + context: %{ + current_user: user + } + } + ) do + with {:is_owned, true, _} <- User.owns_actor(user, actor_id), + {:ok, %Participant{} = participant} <- + Mobilizon.Events.get_participant(event_id, actor_id), + {:only_organizer, false} <- + {:only_organizer, + Mobilizon.Events.list_organizers_participants_for_event(event_id) |> length == 1}, + {:ok, _} <- + Mobilizon.Events.delete_participant(participant) do + {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}} + else + {:is_owned, false} -> + {:error, "Actor id is not owned by authenticated user"} + + {:only_organizer, true} -> + {:error, "You can't leave event because you're the only event creator participant"} + + {:error, :participant_not_found} -> + {:error, "Participant not found"} + end + end + + def actor_leave_event(_parent, _args, _resolution) do + {:error, "You need to be logged-in to leave an event"} + end + @doc """ Search events by title """ @@ -103,7 +182,7 @@ defmodule MobilizonWeb.Resolvers.Event do context: %{current_user: user} }) do with {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id), - {:is_owned, true} <- User.owns_actor(user, actor_id), + {:is_owned, true, _} <- User.owns_actor(user, actor_id), {:event_can_be_managed, true} <- Event.can_event_be_managed_by(event, actor_id), event <- Mobilizon.Events.delete_event!(event) do {:ok, %{id: event.id}} diff --git a/lib/mobilizon_web/resolvers/group.ex b/lib/mobilizon_web/resolvers/group.ex index b063691ae..5a698e2eb 100644 --- a/lib/mobilizon_web/resolvers/group.ex +++ b/lib/mobilizon_web/resolvers/group.ex @@ -82,7 +82,7 @@ defmodule MobilizonWeb.Resolvers.Group do } ) do with {:ok, %Actor{} = group} <- Actors.find_group_by_actor_id(group_id), - {:is_owned, true} <- User.owns_actor(user, actor_id), + {:is_owned, true, _} <- User.owns_actor(user, actor_id), {:ok, %Member{} = member} <- Member.get_member(actor_id, group.id), {:is_admin, true} <- Member.is_administrator(member), group <- Actors.delete_group!(group) do diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex index 564522fe3..8ddc79e34 100644 --- a/lib/mobilizon_web/schema.ex +++ b/lib/mobilizon_web/schema.ex @@ -143,6 +143,7 @@ defmodule MobilizonWeb.Schema do import_fields(:event_mutations) import_fields(:category_mutations) import_fields(:comment_mutations) + import_fields(:participant_mutations) # @desc "Upload a picture" # field :upload_picture, :picture do diff --git a/lib/mobilizon_web/schema/actor.ex b/lib/mobilizon_web/schema/actor.ex index 757252042..76f36b6bf 100644 --- a/lib/mobilizon_web/schema/actor.ex +++ b/lib/mobilizon_web/schema/actor.ex @@ -12,7 +12,7 @@ defmodule MobilizonWeb.Schema.ActorInterface do @desc "An ActivityPub actor" interface :actor do - field(:id, :id, description: "Internal ID for this actor") + field(:id, :integer, description: "Internal ID for this actor") field(:url, :string, description: "The ActivityPub actor's URL") field(:type, :actor_type, description: "The type of Actor (Person, Group,…)") field(:name, :string, description: "The actor's displayed name") diff --git a/lib/mobilizon_web/schema/actors/group.ex b/lib/mobilizon_web/schema/actors/group.ex index 7a24382a9..365f58097 100644 --- a/lib/mobilizon_web/schema/actors/group.ex +++ b/lib/mobilizon_web/schema/actors/group.ex @@ -13,7 +13,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do object :group do interfaces([:actor]) - field(:id, :id, description: "Internal ID for this group") + field(:id, :integer, description: "Internal ID for this group") field(:url, :string, description: "The ActivityPub actor's URL") field(:type, :actor_type, description: "The type of Actor (Person, Group,…)") field(:name, :string, description: "The actor's displayed name") diff --git a/lib/mobilizon_web/schema/actors/person.ex b/lib/mobilizon_web/schema/actors/person.ex index 4397caf6e..2271c26fe 100644 --- a/lib/mobilizon_web/schema/actors/person.ex +++ b/lib/mobilizon_web/schema/actors/person.ex @@ -13,7 +13,7 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do """ object :person do interfaces([:actor]) - field(:id, :id, description: "Internal ID for this person") + field(:id, :integer, description: "Internal ID for this person") field(:user, :user, description: "The user this actor is associated to") field(:member_of, list_of(:member), description: "The list of groups this person is member of") diff --git a/lib/mobilizon_web/schema/event.ex b/lib/mobilizon_web/schema/event.ex index 003dee4bf..70230463b 100644 --- a/lib/mobilizon_web/schema/event.ex +++ b/lib/mobilizon_web/schema/event.ex @@ -12,7 +12,7 @@ defmodule MobilizonWeb.Schema.EventType do @desc "An event" object :event do - field(:id, :id, description: "Internal ID for this event") + field(:id, :integer, description: "Internal ID for this event") field(:uuid, :uuid, description: "The Event UUID") field(:url, :string, description: "The ActivityPub Event URL") field(:local, :boolean, description: "Whether the event is local or not") diff --git a/lib/mobilizon_web/schema/events/participant.ex b/lib/mobilizon_web/schema/events/participant.ex index f811299af..75f9c8954 100644 --- a/lib/mobilizon_web/schema/events/participant.ex +++ b/lib/mobilizon_web/schema/events/participant.ex @@ -5,18 +5,34 @@ defmodule MobilizonWeb.Schema.Events.ParticipantType do use Absinthe.Schema.Notation import Absinthe.Resolution.Helpers, only: [dataloader: 1] alias MobilizonWeb.Resolvers + alias Mobilizon.Events + alias Mobilizon.Actors @desc "Represents a participant to an event" object :participant do - field(:event, :event, + field( + :event, + :event, resolve: dataloader(Events), description: "The event which the actor participates in" ) - field(:actor, :actor, description: "The actor that participates to the event") + field( + :actor, + :actor, + resolve: dataloader(Actors), + description: "The actor that participates to the event" + ) + field(:role, :integer, description: "The role of this actor at this event") end + @desc "Represents a deleted participant" + object :deleted_participant do + field(:event, :deleted_object) + field(:actor, :deleted_object) + end + object :participant_queries do @desc "Get all participants for an event uuid" field :participants, list_of(:participant) do @@ -26,4 +42,22 @@ defmodule MobilizonWeb.Schema.Events.ParticipantType do resolve(&Resolvers.Event.list_participants_for_event/3) end end + + object :participant_mutations do + @desc "Join an event" + field :join_event, :participant do + arg(:event_id, non_null(:integer)) + arg(:actor_id, non_null(:integer)) + + resolve(&Resolvers.Event.actor_join_event/3) + end + + @desc "Leave an event" + field :leave_event, :deleted_participant do + arg(:event_id, non_null(:integer)) + arg(:actor_id, non_null(:integer)) + + resolve(&Resolvers.Event.actor_leave_event/3) + end + end end diff --git a/test/mobilizon_web/resolvers/event_resolver_test.exs b/test/mobilizon_web/resolvers/event_resolver_test.exs index 01a775fee..2c1ee0b08 100644 --- a/test/mobilizon_web/resolvers/event_resolver_test.exs +++ b/test/mobilizon_web/resolvers/event_resolver_test.exs @@ -60,63 +60,6 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do json_response(res, 200)["errors"] end - test "list_participants_for_event/3 returns participants for an event", context do - # Plain event - category = insert(:category) - - event = - @event - |> Map.put(:organizer_actor_id, context.actor.id) - |> Map.put(:category_id, category.id) - - {:ok, event} = Events.create_event(event) - - query = """ - { - participants(uuid: "#{event.uuid}") { - role, - actor { - preferredUsername - } - } - } - """ - - res = - context.conn - |> get("/api", AbsintheHelpers.query_skeleton(query, "participants")) - - assert json_response(res, 200)["data"]["participants"] == [ - %{ - "actor" => %{"preferredUsername" => context.actor.preferred_username}, - "role" => "creator" - } - ] - - # Adding two participants - actor2 = insert(:actor) - actor3 = insert(:actor) - # This one won't get listed (as not approved) - participant = insert(:participant, event: event, actor: actor2, role: :not_approved) - # This one will (as a participant) - participant2 = insert(:participant, event: event, actor: actor3, role: :participant) - - res = - context.conn - |> get("/api", AbsintheHelpers.query_skeleton(query, "participants")) - - assert json_response(res, 200)["data"]["participants"] == [ - %{ - "actor" => %{"preferredUsername" => context.actor.preferred_username}, - "role" => "creator" - }, - %{ - "actor" => %{"preferredUsername" => participant2.actor.preferred_username}, - "role" => "participant" - } - ] - end - test "create_event/3 creates an event", %{conn: conn, actor: actor, user: user} do category = insert(:category) diff --git a/test/mobilizon_web/resolvers/participant_resolver_test.exs b/test/mobilizon_web/resolvers/participant_resolver_test.exs new file mode 100644 index 000000000..ca5d0ddab --- /dev/null +++ b/test/mobilizon_web/resolvers/participant_resolver_test.exs @@ -0,0 +1,372 @@ +defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do + use MobilizonWeb.ConnCase + alias Mobilizon.Events + alias MobilizonWeb.AbsintheHelpers + import Mobilizon.Factory + + @event %{ + description: "some body", + title: "some title", + begins_on: Ecto.DateTime.utc(), + uuid: "b5126423-f1af-43e4-a923-002a03003ba4", + url: "some url" + } + + setup %{conn: conn} do + user = insert(:user) + actor = insert(:actor, user: user, preferred_username: "test") + + {:ok, conn: conn, actor: actor, user: user} + end + + describe "Participant Resolver" do + test "actor_join_event/3 should create a participant", %{conn: conn, user: user, actor: actor} do + event = insert(:event) + + mutation = """ + mutation { + joinEvent( + actor_id: #{actor.id}, + event_id: #{event.id} + ) { + role, + actor { + id + }, + event { + id + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert json_response(res, 200)["errors"] == nil + assert json_response(res, 200)["data"]["joinEvent"]["role"] == "participant" + assert json_response(res, 200)["data"]["joinEvent"]["event"]["id"] == event.id + assert json_response(res, 200)["data"]["joinEvent"]["actor"]["id"] == actor.id + + mutation = """ + mutation { + joinEvent( + actor_id: #{actor.id}, + event_id: #{event.id} + ) { + role + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] =~ "already a participant" + end + + test "actor_join_event/3 should check the actor is owned by the user", %{ + conn: conn, + user: user + } do + event = insert(:event) + + mutation = """ + mutation { + joinEvent( + actor_id: 1042, + event_id: #{event.id} + ) { + role + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] =~ "not owned" + end + + test "actor_join_event/3 should check the event exists", %{ + conn: conn, + user: user, + actor: actor + } do + mutation = """ + mutation { + joinEvent( + actor_id: #{actor.id}, + event_id: 1042 + ) { + role + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] =~ "Event id not found" + end + + test "actor_leave_event/3 should delete a participant from an event", %{ + conn: conn, + user: user, + actor: actor + } do + event = insert(:event, %{organizer_actor: actor}) + participant = insert(:participant, %{actor: actor, event: event}) + participant2 = insert(:participant, %{event: event}) + + mutation = """ + mutation { + leaveEvent( + actor_id: #{participant.actor.id}, + event_id: #{event.id} + ) { + actor { + id + }, + event { + id + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert json_response(res, 200)["errors"] == nil + assert json_response(res, 200)["data"]["leaveEvent"]["event"]["id"] == event.id + assert json_response(res, 200)["data"]["leaveEvent"]["actor"]["id"] == participant.actor.id + + query = """ + { + participants(uuid: "#{event.uuid}") { + role, + actor { + preferredUsername + } + } + } + """ + + res = + conn + |> get("/api", AbsintheHelpers.query_skeleton(query, "participants")) + + assert json_response(res, 200)["data"]["participants"] == [ + %{ + "actor" => %{"preferredUsername" => participant2.actor.preferred_username}, + "role" => "creator" + } + ] + end + + test "actor_leave_event/3 should check if the participant is the only creator", %{ + conn: conn, + actor: actor, + user: user + } do + participant = insert(:participant, %{actor: actor}) + + mutation = """ + mutation { + leaveEvent( + actor_id: #{participant.actor.id}, + event_id: #{participant.event.id} + ) { + actor { + id + }, + event { + id + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] == + "You can't leave event because you're the only event creator participant" + + # If we have a second participant but not an event creator + insert(:participant, %{event: participant.event, role: :participant}) + + mutation = """ + mutation { + leaveEvent( + actor_id: #{participant.actor.id}, + event_id: #{participant.event.id} + ) { + actor { + id + }, + event { + id + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] == + "You can't leave event because you're the only event creator participant" + end + + test "actor_leave_event/3 should check the user is logged in", %{conn: conn, actor: actor} do + participant = insert(:participant, %{actor: actor}) + + mutation = """ + mutation { + leaveEvent( + actor_id: #{participant.actor.id}, + event_id: #{participant.event.id} + ) { + actor { + id + } + } + } + """ + + res = + conn + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] =~ "logged-in" + end + + test "actor_leave_event/3 should check the actor is owned by the user", %{ + conn: conn, + user: user + } do + participant = insert(:participant) + + mutation = """ + mutation { + leaveEvent( + actor_id: #{participant.actor.id}, + event_id: #{participant.event.id} + ) { + actor { + id + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] =~ "not owned" + end + + test "actor_leave_event/3 should check the participant exists", %{ + conn: conn, + user: user, + actor: actor + } do + participant = insert(:participant, %{actor: actor}) + + mutation = """ + mutation { + leaveEvent( + actor_id: #{participant.actor.id}, + event_id: 1042 + ) { + actor { + id + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] =~ "Participant not found" + end + + test "list_participants_for_event/3 returns participants for an event", context do + # Plain event + category = insert(:category) + + event = + @event + |> Map.put(:organizer_actor_id, context.actor.id) + |> Map.put(:category_id, category.id) + + {:ok, event} = Events.create_event(event) + + query = """ + { + participants(uuid: "#{event.uuid}") { + role, + actor { + preferredUsername + } + } + } + """ + + res = + context.conn + |> get("/api", AbsintheHelpers.query_skeleton(query, "participants")) + + assert json_response(res, 200)["data"]["participants"] == [ + %{ + "actor" => %{"preferredUsername" => context.actor.preferred_username}, + "role" => "creator" + } + ] + + # Adding two participants + actor2 = insert(:actor) + actor3 = insert(:actor) + # This one won't get listed (as not approved) + insert(:participant, event: event, actor: actor2, role: :not_approved) + # This one will (as a participant) + participant2 = insert(:participant, event: event, actor: actor3, role: :participant) + + res = + context.conn + |> get("/api", AbsintheHelpers.query_skeleton(query, "participants")) + + assert json_response(res, 200)["data"]["participants"] == [ + %{ + "actor" => %{"preferredUsername" => context.actor.preferred_username}, + "role" => "creator" + }, + %{ + "actor" => %{"preferredUsername" => participant2.actor.preferred_username}, + "role" => "participant" + } + ] + end + end +end