diff --git a/lib/mobilizon_web/api/events.ex b/lib/mobilizon_web/api/events.ex index 5e138eef8..45e107022 100644 --- a/lib/mobilizon_web/api/events.ex +++ b/lib/mobilizon_web/api/events.ex @@ -12,28 +12,69 @@ defmodule MobilizonWeb.API.Events do Create an event """ @spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any() - def create_event( - %{ - begins_on: begins_on, - description: description, - options: options, - organizer_actor_id: organizer_actor_id, - tags: tags, - title: title - } = args - ) - when is_map(options) do - with %Actor{url: url} = actor <- - Actors.get_local_actor_with_everything(organizer_actor_id), - physical_address <- Map.get(args, :physical_address, nil), - title <- String.trim(title), - visibility <- Map.get(args, :visibility, :public), - picture <- Map.get(args, :picture, nil), - {content_html, tags, to, cc} <- - Utils.prepare_content(actor, description, visibility, tags, nil), + def create_event(%{organizer_actor: organizer_actor} = args) do + with %{ + title: title, + physical_address: physical_address, + visibility: visibility, + picture: picture, + content_html: content_html, + tags: tags, + to: to, + cc: cc, + begins_on: begins_on, + category: category, + options: options + } <- prepare_args(args), event <- ActivityPubUtils.make_event_data( - url, + organizer_actor.url, + %{to: to, cc: cc}, + title, + content_html, + picture, + tags, + %{begins_on: begins_on, physical_address: physical_address, category: category, options: options} + ) do + ActivityPub.create(%{ + to: ["https://www.w3.org/ns/activitystreams#Public"], + actor: organizer_actor, + object: event, + local: true + }) + end + end + + @doc """ + Update an event + """ + @spec update_event(map()) :: {:ok, Activity.t(), Event.t()} | any() + def update_event( + %{ + organizer_actor: organizer_actor, + event: event + } = args + ) do + with %{ + title: title, + physical_address: physical_address, + visibility: visibility, + picture: picture, + content_html: content_html, + tags: tags, + to: to, + cc: cc, + begins_on: begins_on, + category: category, + options: options + } <- + prepare_args( + args + |> update_args(event) + ), + event <- + ActivityPubUtils.make_event_data( + organizer_actor.url, %{to: to, cc: cc}, title, content_html, @@ -46,12 +87,60 @@ defmodule MobilizonWeb.API.Events do options: options } ) do - ActivityPub.create(%{ + ActivityPub.update(%{ to: ["https://www.w3.org/ns/activitystreams#Public"], - actor: actor, + actor: organizer_actor, object: event, local: true }) end end + + defp update_args(args, event) do + %{ + title: Map.get(args, :title, event.title), + description: Map.get(args, :description, event.description), + tags: Map.get(args, :tags, event.tags), + physical_address: Map.get(args, :physical_address, event.physical_address), + visibility: Map.get(args, :visibility, event.visibility), + physical_address: Map.get(args, :physical_address, event.physical_address), + begins_on: Map.get(args, :begins_on, event.begins_on), + category: Map.get(args, :category, event.category), + options: Map.get(args, :options, event.options) + } + end + + defp prepare_args( + %{ + organizer_actor: organizer_actor, + title: title, + description: description, + options: options, + tags: tags, + begins_on: begins_on, + category: category, + options: options + } = args + ) do + with physical_address <- Map.get(args, :physical_address, nil), + title <- String.trim(title), + visibility <- Map.get(args, :visibility, :public), + picture <- Map.get(args, :picture, nil), + {content_html, tags, to, cc} <- + Utils.prepare_content(organizer_actor, description, visibility, tags, nil) do + %{ + title: title, + physical_address: physical_address, + visibility: visibility, + picture: picture, + content_html: content_html, + tags: tags, + to: to, + cc: cc, + begins_on: begins_on, + category: category, + options: options + } + end + end end diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index 8a01b2cf0..d34402f0f 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -58,7 +58,8 @@ defmodule MobilizonWeb.Resolvers.Event do ) do # We get the organizer's next public event events = - [Events.get_actor_upcoming_public_event(organizer_actor, uuid)] |> Enum.filter(&is_map/1) + [Events.get_actor_upcoming_public_event(organizer_actor, uuid)] + |> Enum.filter(&is_map/1) # We find similar events with the same tags # uniq_by : It's possible event_from_same_actor is inside events_from_tags @@ -150,7 +151,17 @@ defmodule MobilizonWeb.Resolvers.Event do {:has_event, {:ok, %Event{} = event}} <- {:has_event, Mobilizon.Events.get_event_full(event_id)}, {:ok, _activity, _participant} <- MobilizonWeb.API.Participations.leave(event, actor) do - {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}} + { + :ok, + %{ + event: %{ + id: event_id + }, + actor: %{ + id: actor_id + } + } + } else {:has_event, _} -> {:error, "Event with this ID #{inspect(event_id)} doesn't exist"} @@ -173,12 +184,33 @@ defmodule MobilizonWeb.Resolvers.Event do @doc """ Create an event """ - def create_event(_parent, args, %{context: %{current_user: _user}} = _resolution) do - with {:ok, args} <- save_attached_picture(args), + def create_event( + _parent, + %{organizer_actor_id: organizer_actor_id} = args, + %{ + context: %{ + current_user: user + } + } = _resolution + ) do + with {:is_owned, true, organizer_actor} <- User.owns_actor(user, organizer_actor_id), + {:ok, args} <- save_attached_picture(args), {:ok, args} <- save_physical_address(args), - {:ok, %Activity{data: %{"object" => %{"type" => "Event"} = _object}}, %Event{} = event} <- - MobilizonWeb.API.Events.create_event(args) do + args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor), + { + :ok, + %Activity{ + data: %{ + "object" => %{"type" => "Event"} = _object + } + }, + %Event{} = event + } <- + MobilizonWeb.API.Events.create_event(args_with_organizer) do {:ok, event} + else + {:is_owned, false} -> + {:error, "Organizer actor id is not owned by the user"} end end @@ -186,19 +218,66 @@ defmodule MobilizonWeb.Resolvers.Event do {:error, "You need to be logged-in to create events"} end + @doc """ + Update an event + """ + def update_event( + _parent, + %{event_id: event_id} = args, + %{ + context: %{ + current_user: user + } + } = _resolution + ) do + with {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id), + {:is_owned, true, organizer_actor} <- User.owns_actor(user, event.organizer_actor_id), + {:ok, args} <- save_attached_picture(args), + {:ok, args} <- save_physical_address(args), + { + :ok, + %Activity{ + data: %{ + "object" => %{"type" => "Event"} = _object + } + }, + %Event{} = event + } <- + MobilizonWeb.API.Events.update_event(args) do + {:ok, event} + else + {:error, :event_not_found} -> + {:error, "Event not found"} + end + end + + def update_event(_parent, _args, _resolution) do + {:error, "You need to be logged-in to update an event"} + end + # If we have an attached picture, just transmit it. It will be handled by # Mobilizon.Service.ActivityPub.Utils.make_picture_data/1 # However, we need to pass it's actor ID @spec save_attached_picture(map()) :: {:ok, map()} defp save_attached_picture( - %{picture: %{picture: %{file: %Plug.Upload{} = _picture} = all_pic}} = args + %{ + picture: %{ + picture: %{file: %Plug.Upload{} = _picture} = all_pic + } + } = args ) do {:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor_id))} end # Otherwise if we use a previously uploaded picture we need to fetch it from database @spec save_attached_picture(map()) :: {:ok, map()} - defp save_attached_picture(%{picture: %{picture_id: picture_id}} = args) do + defp save_attached_picture( + %{ + picture: %{ + picture_id: picture_id + } + } = args + ) do with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do {:ok, Map.put(args, :picture, picture)} end @@ -208,7 +287,13 @@ defmodule MobilizonWeb.Resolvers.Event do defp save_attached_picture(args), do: {:ok, args} @spec save_physical_address(map()) :: {:ok, map()} - defp save_physical_address(%{physical_address: %{url: physical_address_url}} = args) + defp save_physical_address( + %{ + physical_address: %{ + url: physical_address_url + } + } = args + ) when not is_nil(physical_address_url) do with %Address{} = address <- Addresses.get_address_by_url(physical_address_url), args <- Map.put(args, :physical_address, address.url) do @@ -230,9 +315,15 @@ defmodule MobilizonWeb.Resolvers.Event do @doc """ Delete an event """ - def delete_event(_parent, %{event_id: event_id, actor_id: actor_id}, %{ - context: %{current_user: user} - }) do + def delete_event( + _parent, + %{event_id: event_id, actor_id: actor_id}, + %{ + context: %{ + current_user: user + } + } + ) do with {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id), {:is_owned, true, _} <- User.owns_actor(user, actor_id), {:event_can_be_managed, true} <- Event.can_event_be_managed_by(event, actor_id), diff --git a/lib/mobilizon_web/schema/event.ex b/lib/mobilizon_web/schema/event.ex index 8cbed015e..5c9956a6a 100644 --- a/lib/mobilizon_web/schema/event.ex +++ b/lib/mobilizon_web/schema/event.ex @@ -234,6 +234,35 @@ defmodule MobilizonWeb.Schema.EventType do resolve(&Event.create_event/3) end + @desc "Update an event" + field :update_event, type: :event do + arg(:event_id, non_null(:integer)) + + arg(:title, :string) + arg(:description, :string) + arg(:begins_on, :datetime) + arg(:ends_on, :datetime) + arg(:state, :integer) + arg(:status, :integer) + arg(:public, :boolean) + arg(:visibility, :event_visibility) + + arg(:tags, list_of(:string), description: "The list of tags associated to the event") + + arg(:picture, :picture_input, + description: + "The picture for the event, either as an object or directly the ID of an existing Picture" + ) + + arg(:publish_at, :datetime) + arg(:online_address, :string) + arg(:phone_address, :string) + arg(:category, :string) + arg(:physical_address, :address_input) + + resolve(&Event.update_event/3) + end + @desc "Delete an event" field :delete_event, :deleted_object do arg(:event_id, non_null(:integer)) diff --git a/test/mobilizon_web/resolvers/event_resolver_test.exs b/test/mobilizon_web/resolvers/event_resolver_test.exs index 077f2ec05..7aa69929e 100644 --- a/test/mobilizon_web/resolvers/event_resolver_test.exs +++ b/test/mobilizon_web/resolvers/event_resolver_test.exs @@ -58,6 +58,40 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do json_response(res, 200)["errors"] end + test "create_event/3 should check the organizer_actor_id is owned by the user", %{ + conn: conn, + user: user + } do + another_actor = insert(:actor) + + begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() + + mutation = """ + mutation { + createEvent( + title: "come to my event", + description: "it will be fine", + begins_on: "#{begins_on}", + organizer_actor_id: "#{another_actor.id}", + category: "birthday" + ) { + title, + uuid + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert json_response(res, 200)["data"]["createEvent"] == nil + + assert hd(json_response(res, 200)["errors"])["message"] == + "Organizer actor id is not owned by the user" + end + test "create_event/3 creates an event", %{conn: conn, actor: actor, user: user} do mutation = """ mutation { @@ -384,6 +418,100 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do assert json_response(res, 200)["data"]["createEvent"]["picture"]["url"] end + test "update_event/3 should check the event exists", %{conn: conn, actor: actor, user: user} do + mutation = """ + mutation { + updateEvent( + event_id: 45, + title: "my event updated", + ) { + title, + uuid, + tags { + title, + slug + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] == "Event not found" + end + + test "update_event/3 should check the user is an administrator", %{ + conn: conn, + actor: actor, + user: user + } do + event = insert(:event) + + mutation = """ + mutation { + updateEvent( + title: "my event updated", + ) { + title, + uuid, + tags { + title, + slug + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert json_response(res, 200)["errors"] == nil + end + + test "update_event/3 updates an event", %{conn: conn, actor: actor, user: user} do + event = insert(:event) + + begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() + + mutation = """ + mutation { + updateEvent( + title: "my event updated", + description: "description updated", + begins_on: "#{begins_on}", + organizer_actor_id: "#{actor.id}", + category: "birthday", + tags: ["tag1_updated", "tag2_updated"] + ) { + title, + uuid, + tags { + title, + slug + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert json_response(res, 200)["errors"] == nil + assert json_response(res, 200)["data"]["updateEvent"]["title"] == "my event updated" + + assert json_response(res, 200)["data"]["createEvent"]["tags"] == [ + %{"slug" => "tag1_updated", "title" => "tag1_updated"}, + %{"slug" => "tag2_updated", "title" => "tag2_updated"} + ] + end + test "list_events/3 returns events", context do event = insert(:event)