diff --git a/js/src/components/Event/AddressAutoComplete.vue b/js/src/components/Event/AddressAutoComplete.vue index 5dc47ea5b..76197a2d0 100644 --- a/js/src/components/Event/AddressAutoComplete.vue +++ b/js/src/components/Event/AddressAutoComplete.vue @@ -81,11 +81,12 @@ import { Modal } from 'buefy/dist/components/dialog'; export default class AddressAutoComplete extends Vue { @Prop({ required: false, default: () => [] }) initialData!: IAddress[]; + @Prop({ required: false }) value!: IAddress; data: IAddress[] = this.initialData; selected: IAddress|null = new Address(); isFetching: boolean = false; - queryText: string = ''; + queryText: string = this.value && this.value.description || ''; addressModalActive: boolean = false; async getAsyncData(query) { diff --git a/js/src/components/Event/TagInput.vue b/js/src/components/Event/TagInput.vue index 0a5f556df..385f4d38f 100644 --- a/js/src/components/Event/TagInput.vue +++ b/js/src/components/Event/TagInput.vue @@ -1,33 +1,51 @@ <template> <b-field label="Enter some tags"> <b-taginput - v-model="tags" + v-model="tagsStrings" :data="filteredTags" autocomplete :allow-new="true" :field="path" icon="label" placeholder="Add a tag" - @typing="getFilteredTags"> + @typing="getFilteredTags" + > </b-taginput> </b-field> </template> <script lang="ts"> -import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; -import { get } from 'lodash'; +import { Component, Prop, Vue } from 'vue-property-decorator'; +import { get, differenceBy } from 'lodash'; import { ITag } from '@/types/tag.model'; -@Component + +@Component({ + computed: { + tagsStrings: { + get() { + return this.$props.data.map((tag: ITag) => tag.title); + }, + set(tagStrings) { + const tagEntities = tagStrings.map((tag) => { + if (TagInput.isTag(tag)) { + return tag; + } + return { title: tag, slug: tag } as ITag; + }); + this.$emit('input', tagEntities); + }, + }, + }, +}) export default class TagInput extends Vue { - @Prop({ required: false, default: () => [] }) data!: object[]; + @Prop({ required: false, default: () => [] }) data!: ITag[]; @Prop({ required: true, default: 'value' }) path!: string; - @Prop({ required: true }) value!: string; + @Prop({ required: true }) value!: ITag[]; - filteredTags: object[] = []; - tags: object[] = []; + filteredTags: ITag[] = []; getFilteredTags(text) { - this.filteredTags = this.data.filter((option) => { + this.filteredTags = differenceBy(this.data, this.value, 'id').filter((option) => { return get(option, this.path) .toString() .toLowerCase() @@ -35,18 +53,6 @@ export default class TagInput extends Vue { }); } - @Watch('tags') - onTagsChanged (tags) { - const tagEntities = tags.map((tag) => { - if (TagInput.isTag(tag)) { - return tag; - } - return { title: tag, slug: tag } as ITag; - }); - console.log('tags changed', tagEntities); - this.$emit('input', tagEntities); - } - static isTag(x: any): x is ITag { return x.slug !== undefined; } diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index a2374bb15..eac64045d 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -52,6 +52,7 @@ export const FETCH_EVENT = gql` domain, name, url, + id, }, # attributedTo { # avatar { @@ -64,6 +65,7 @@ export const FETCH_EVENT = gql` ${participantQuery} }, tags { + id, slug, title }, @@ -82,6 +84,25 @@ export const FETCH_EVENT = gql` domain, name, } + }, + options { + maximumAttendeeCapacity, + remainingAttendeeCapacity, + showRemainingAttendeeCapacity, + offers { + price, + priceCurrency, + url + }, + participationConditions { + title, + content, + url + }, + attendees, + program, + commentModeration, + showParticipationPrice } } } @@ -144,6 +165,7 @@ export const CREATE_EVENT = gql` $organizerActorId: ID!, $category: String, $beginsOn: DateTime!, + $endsOn: DateTime, $picture: PictureInput, $tags: [String], $options: EventOptionsInput, @@ -154,6 +176,7 @@ export const CREATE_EVENT = gql` title: $title, description: $description, beginsOn: $beginsOn, + endsOn: $endsOn, organizerActorId: $organizerActorId, category: $category, options: $options, @@ -173,13 +196,32 @@ export const CREATE_EVENT = gql` `; export const EDIT_EVENT = gql` - mutation EditEvent( + mutation updateEvent( + $id: ID!, $title: String!, $description: String!, - $organizerActorId: Int!, - $category: String + $organizerActorId: ID!, + $category: String, + $beginsOn: DateTime!, + $endsOn: DateTime, + $picture: PictureInput, + $tags: [String], + $options: EventOptionsInput, + $physicalAddress: AddressInput, + $visibility: EventVisibility ) { - EditEvent(title: $title, description: $description, organizerActorId: $organizerActorId, category: $category) { + updateEvent(eventId: $id, + title: $title, + description: $description, + beginsOn: $beginsOn, + endsOn: $endsOn, + organizerActorId: $organizerActorId, + category: $category, + options: $options, + picture: $picture, + tags: $tags, + physicalAddress: $physicalAddress, + visibility: $visibility) { uuid } } diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index d468b6299..b92244dde 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -4,9 +4,9 @@ import { ITag } from '@/types/tag.model'; import { IPicture } from '@/types/picture.model'; export enum EventStatus { - TENTATIVE, - CONFIRMED, - CANCELLED, + TENTATIVE = 'TENTATIVE', + CONFIRMED = 'CONFIRMED', + CANCELLED = 'CANCELLED', } export enum EventVisibility { @@ -17,9 +17,9 @@ export enum EventVisibility { } export enum EventJoinOptions { - FREE, - RESTRICTED, - INVITE, + FREE = 'FREE', + RESTRICTED = 'RESTRICTED', + INVITE = 'INVITE', } export enum EventVisibilityJoinOptions { diff --git a/js/src/views/Event/Edit.vue b/js/src/views/Event/Edit.vue index b8f24d3c0..56bf130b9 100644 --- a/js/src/views/Event/Edit.vue +++ b/js/src/views/Event/Edit.vue @@ -177,7 +177,7 @@ <script lang="ts"> import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; -import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, IEvent, CommentModeration } from '@/types/event.model'; +import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, CommentModeration } from '@/types/event.model'; import { LOGGED_PERSON } from '@/graphql/actor'; import { IPerson, Person } from '@/types/actor'; import PictureUpload from '@/components/PictureUpload.vue'; @@ -207,6 +207,7 @@ export default class EditEvent extends Vue { eventId!: string | undefined; loggedPerson = new Person(); + tags: ITag[] = []; event = new EventModel(); pictureFile: File | null = null; @@ -223,7 +224,7 @@ export default class EditEvent extends Vue { @Watch('$route.params.eventId', { immediate: true }) async onEventIdParamChanged (val: string) { - if (this.isUpdate !== true) return; + if (!this.isUpdate) return; this.eventId = val; @@ -231,6 +232,7 @@ export default class EditEvent extends Vue { this.event = await this.getEvent(); this.pictureFile = await buildFileFromIPicture(this.event.picture); + this.limitedPlaces = this.event.options.maximumAttendeeCapacity != null; } } @@ -241,7 +243,6 @@ export default class EditEvent extends Vue { this.event.beginsOn = now; this.event.endsOn = end; - console.log('eventvisibilityjoinoptions', this.eventVisibilityJoinOptions); } createOrUpdate(e: Event) { @@ -261,7 +262,7 @@ export default class EditEvent extends Vue { console.log('Event created', data); - this.$router.push({ + await this.$router.push({ name: 'Event', params: { uuid: data.createEvent.uuid }, }); @@ -277,7 +278,7 @@ export default class EditEvent extends Vue { variables: this.buildVariables(), }); - this.$router.push({ + await this.$router.push({ name: 'Event', params: { uuid: this.eventId as string }, }); @@ -297,6 +298,8 @@ export default class EditEvent extends Vue { }; const res = Object.assign({}, this.event, obj); + delete this.event.options['__typename']; + if (this.event.physicalAddress) { delete this.event.physicalAddress['__typename']; } diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue index efaee349a..15589877e 100644 --- a/js/src/views/Event/Event.vue +++ b/js/src/views/Event/Event.vue @@ -57,7 +57,7 @@ <p class="control"> <router-link class="button" - :to="{ name: 'EditEvent', params: {uuid: event.uuid}}" + :to="{ name: 'EditEvent', params: {eventId: event.uuid}}" > <translate>Edit</translate> </router-link> diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index ba4477e8f..2cf983479 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -110,6 +110,24 @@ defmodule Mobilizon.Actors.Actor do |> unique_constraint(:url, name: :actors_url_index) end + @doc false + def update_changeset(%Actor{} = actor, attrs) do + actor + |> Ecto.Changeset.cast(attrs, [ + :name, + :summary, + :keys, + :manually_approves_followers, + :suspended, + :user_id + ]) + |> cast_embed(:avatar) + |> cast_embed(:banner) + |> validate_required([:preferred_username, :keys, :suspended, :url]) + |> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) + |> unique_constraint(:url, name: :actors_url_index) + end + @doc """ Changeset for person registration """ diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 5cd0e490b..7ab1a9b0b 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -124,7 +124,7 @@ defmodule Mobilizon.Actors do """ def update_actor(%Actor{} = actor, attrs) do actor - |> Actor.changeset(attrs) + |> Actor.update_changeset(attrs) |> delete_files_if_media_changed() |> Repo.update() end diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index 3521ede04..52d1c765d 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -54,10 +54,10 @@ defmodule Mobilizon.Events.Event do field(:online_address, :string) field(:phone_address, :string) field(:category, :string) - embeds_one(:options, Mobilizon.Events.EventOptions) + embeds_one(:options, Mobilizon.Events.EventOptions, on_replace: :update) belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id) belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id) - many_to_many(:tags, Tag, join_through: "events_tags") + many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete) many_to_many(:participants, Actor, join_through: Participant) has_many(:tracks, Track) has_many(:sessions, Session) @@ -98,6 +98,38 @@ defmodule Mobilizon.Events.Event do ]) end + @doc false + def update_changeset(%Event{} = event, attrs) do + event + |> Ecto.Changeset.cast(attrs, [ + :title, + :slug, + :description, + :begins_on, + :ends_on, + :category, + :status, + :visibility, + :publish_at, + :online_address, + :phone_address, + :picture_id, + :physical_address_id + ]) + |> cast_embed(:options) + |> put_tags(attrs) + |> validate_required([ + :title, + :begins_on, + :organizer_actor_id, + :url, + :uuid + ]) + end + + defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags) + defp put_tags(changeset, _), do: changeset + def can_event_be_managed_by(%Event{organizer_actor_id: organizer_actor_id}, actor_id) when organizer_actor_id == actor_id do {:event_can_be_managed, true} diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index ccb198339..cc6d71702 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -227,11 +227,12 @@ defmodule Mobilizon.Events do :tracks, :tags, :participants, - :physical_address + :physical_address, + :picture ])} - err -> - {:error, err} + _err -> + {:error, :event_not_found} end end @@ -435,7 +436,8 @@ defmodule Mobilizon.Events do """ def update_event(%Event{} = event, attrs) do event - |> Event.changeset(attrs) + |> Repo.preload(:tags) + |> Event.update_changeset(attrs) |> Repo.update() end diff --git a/lib/mobilizon_web/api/events.ex b/lib/mobilizon_web/api/events.ex index 5e138eef8..f841d2597 100644 --- a/lib/mobilizon_web/api/events.ex +++ b/lib/mobilizon_web/api/events.ex @@ -2,8 +2,7 @@ defmodule MobilizonWeb.API.Events do @moduledoc """ API for Events """ - alias Mobilizon.Actors - alias Mobilizon.Actors.Actor + alias Mobilizon.Events.Event alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils alias MobilizonWeb.API.Utils @@ -12,28 +11,23 @@ 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, + picture: picture, + content_html: content_html, + tags: tags, + to: to, + cc: cc, + begins_on: begins_on, + ends_on: ends_on, + category: category, + options: options + } <- prepare_args(args), event <- ActivityPubUtils.make_event_data( - url, + organizer_actor.url, %{to: to, cc: cc}, title, content_html, @@ -41,17 +35,104 @@ defmodule MobilizonWeb.API.Events do tags, %{ begins_on: begins_on, + ends_on: ends_on, physical_address: physical_address, - category: Map.get(args, :category), + category: category, options: options } ) do ActivityPub.create(%{ to: ["https://www.w3.org/ns/activitystreams#Public"], - actor: actor, + actor: organizer_actor, object: event, local: true }) end end + + @doc """ + Update an event + """ + @spec update_event(map(), Event.t()) :: {:ok, Activity.t(), Event.t()} | any() + def update_event( + %{ + organizer_actor: organizer_actor + } = args, + %Event{} = event + ) do + with args <- Map.put(args, :tags, Map.get(args, :tags, [])), + %{ + title: title, + physical_address: physical_address, + picture: picture, + content_html: content_html, + tags: tags, + to: to, + cc: cc, + begins_on: begins_on, + ends_on: ends_on, + category: category, + options: options + } <- + prepare_args(Map.merge(event, args)), + event <- + ActivityPubUtils.make_event_data( + organizer_actor.url, + %{to: to, cc: cc}, + title, + content_html, + picture, + tags, + %{ + begins_on: begins_on, + ends_on: ends_on, + physical_address: physical_address, + category: category, + options: options + }, + event.uuid, + event.url + ) do + ActivityPub.update(%{ + to: ["https://www.w3.org/ns/activitystreams#Public"], + actor: organizer_actor.url, + cc: [], + object: event, + local: true + }) + end + end + + defp prepare_args( + %{ + organizer_actor: organizer_actor, + title: title, + description: description, + options: options, + tags: tags, + begins_on: begins_on, + category: category + } = 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, + picture: picture, + content_html: content_html, + tags: tags, + to: to, + cc: cc, + begins_on: begins_on, + ends_on: Map.get(args, :ends_on, nil), + category: category, + options: options + } + end + end end diff --git a/lib/mobilizon_web/api/groups.ex b/lib/mobilizon_web/api/groups.ex index 5033792b3..7ff870670 100644 --- a/lib/mobilizon_web/api/groups.ex +++ b/lib/mobilizon_web/api/groups.ex @@ -18,13 +18,13 @@ defmodule MobilizonWeb.API.Groups do preferred_username: title, summary: summary, creator_actor_id: creator_actor_id, - avatar: avatar, - banner: banner + avatar: _avatar, + banner: _banner } = args ) do with {:is_owned, true, actor} <- User.owns_actor(user, creator_actor_id), - {:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)}, title <- String.trim(title), + {:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)}, visibility <- Map.get(args, :visibility, :public), {content_html, tags, to, cc} <- Utils.prepare_content(actor, summary, visibility, [], nil), diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index 8a01b2cf0..8eadf44a6 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,35 @@ 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 + # See https://github.com/absinthe-graphql/absinthe/issues/490 + with args <- Map.put(args, :options, args[:options] || %{}), + {: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 +220,72 @@ 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 + # See https://github.com/absinthe-graphql/absinthe/issues/490 + with args <- Map.put(args, :options, args[:options] || %{}), + {:ok, %Event{} = event} <- Mobilizon.Events.get_event_full(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), + args <- Map.put(args, :organizer_actor, organizer_actor), + { + :ok, + %Activity{ + data: %{ + "object" => %{"type" => "Event"} = _object + } + }, + %Event{} = event + } <- + MobilizonWeb.API.Events.update_event(args, event) do + {:ok, event} + else + {:error, :event_not_found} -> + {:error, "Event not found"} + + {:is_owned, _} -> + {:error, "User doesn't own actor"} + 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 +295,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 +323,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..f5d2020b3 100644 --- a/lib/mobilizon_web/schema/event.ex +++ b/lib/mobilizon_web/schema/event.ex @@ -229,11 +229,42 @@ defmodule MobilizonWeb.Schema.EventType do arg(:organizer_actor_id, non_null(:id)) arg(:category, :string, default_value: "meeting") arg(:physical_address, :address_input) - arg(:options, :event_options_input, default_value: %{}) + arg(:options, :event_options_input) resolve(&Event.create_event/3) end + @desc "Update an event" + field :update_event, type: :event do + arg(:event_id, non_null(:id)) + + 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(:organizer_actor_id, :id) + + 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) + arg(:options, :event_options_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/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index 3823d7c33..6cad45aab 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -39,24 +39,16 @@ defmodule Mobilizon.Service.ActivityPub do @doc """ Wraps an object into an activity """ - # TODO: Rename me - @spec insert(map(), boolean()) :: {:ok, %Activity{}} | {:error, any()} - def insert(map, local \\ true) when is_map(map) do - with map <- lazy_put_activity_defaults(map), - {:ok, object} <- insert_full_object(map) do - activity = %Activity{ - data: map, - local: local, - actor: map["actor"], - recipients: get_recipients(map) - } - - # Notification.create_notifications(activity) - # stream_out(activity) - {:ok, activity, object} - else - %Activity{} = activity -> {:ok, activity} - error -> {:error, error} + @spec create_activity(map(), boolean()) :: {:ok, %Activity{}} + def create_activity(map, local \\ true) when is_map(map) do + with map <- lazy_put_activity_defaults(map) do + {:ok, + %Activity{ + data: map, + local: local, + actor: map["actor"], + recipients: get_recipients(map) + }} end end @@ -137,7 +129,8 @@ defmodule Mobilizon.Service.ActivityPub do %{to: to, actor: actor, published: published, object: object}, additional ), - {:ok, activity, object} <- insert(create_data, local), + {:ok, activity} <- create_activity(create_data, local), + {:ok, object} <- insert_full_object(create_data), :ok <- maybe_federate(activity) do # {:ok, actor} <- Actors.increase_event_count(actor) do {:ok, activity, object} @@ -160,7 +153,8 @@ defmodule Mobilizon.Service.ActivityPub do "object" => object, "id" => activity_wrapper_id || get_url(object) <> "/activity" }, - {:ok, activity, object} <- insert(data, local), + {:ok, activity} <- create_activity(data, local), + {:ok, object} <- insert_full_object(data), :ok <- maybe_federate(activity) do {:ok, activity, object} end @@ -177,7 +171,8 @@ defmodule Mobilizon.Service.ActivityPub do "object" => object, "id" => activity_wrapper_id || get_url(object) <> "/activity" }, - {:ok, activity, object} <- insert(data, local), + {:ok, activity} <- create_activity(data, local), + {:ok, object} <- insert_full_object(data), :ok <- maybe_federate(activity) do {:ok, activity, object} end @@ -195,7 +190,8 @@ defmodule Mobilizon.Service.ActivityPub do "actor" => actor, "object" => object }, - {:ok, activity, object} <- insert(data, local), + {:ok, activity} <- create_activity(data, local), + {:ok, object} <- update_object(object["id"], data), :ok <- maybe_federate(activity) do {:ok, activity, object} end @@ -210,7 +206,8 @@ defmodule Mobilizon.Service.ActivityPub do # ) do # with nil <- get_existing_like(url, object), # like_data <- make_like_data(user, object, activity_id), - # {:ok, activity, object} <- insert(like_data, local), + # {:ok, activity} <- create_activity(like_data, local), + # {:ok, object} <- insert_full_object(data), # {:ok, object} <- add_like_to_object(activity, object), # :ok <- maybe_federate(activity) do # {:ok, activity, object} @@ -228,7 +225,8 @@ defmodule Mobilizon.Service.ActivityPub do # ) do # with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object), # unlike_data <- make_unlike_data(actor, like_activity, activity_id), - # {:ok, unlike_activity, _object} <- insert(unlike_data, local), + # {:ok, unlike_activity} <- create_activity(unlike_data, local), + # {:ok, _object} <- insert_full_object(data), # {:ok, _activity} <- Repo.delete(like_activity), # {:ok, object} <- remove_like_from_object(like_activity, object), # :ok <- maybe_federate(unlike_activity) do @@ -247,7 +245,8 @@ defmodule Mobilizon.Service.ActivityPub do ) do with true <- is_public?(object), announce_data <- make_announce_data(actor, object, activity_id, public), - {:ok, activity, object} <- insert(announce_data, local), + {:ok, activity} <- create_activity(announce_data, local), + {:ok, object} <- insert_full_object(announce_data), :ok <- maybe_federate(activity) do {:ok, activity, object} else @@ -265,7 +264,8 @@ defmodule Mobilizon.Service.ActivityPub do ) do with announce_activity <- make_announce_data(actor, object, cancelled_activity_id), unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id), - {:ok, unannounce_activity, _object} <- insert(unannounce_data, local), + {:ok, unannounce_activity} <- create_activity(unannounce_data, local), + {:ok, object} <- insert_full_object(unannounce_data), :ok <- maybe_federate(unannounce_activity) do {:ok, unannounce_activity, object} else @@ -282,7 +282,8 @@ defmodule Mobilizon.Service.ActivityPub do activity_follow_id <- activity_id || follow_url, data <- make_follow_data(followed, follower, activity_follow_id), - {:ok, activity, object} <- insert(data, local), + {:ok, activity} <- create_activity(data, local), + {:ok, object} <- insert_full_object(data), :ok <- maybe_federate(activity) do {:ok, activity, object} else @@ -304,12 +305,14 @@ defmodule Mobilizon.Service.ActivityPub do follower, "#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity" ), - {:ok, follow_activity, _object} <- insert(data, local), + {:ok, follow_activity} <- create_activity(data, local), + {:ok, _object} <- insert_full_object(data), activity_unfollow_id <- activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity", unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id), - {:ok, activity, object} <- insert(unfollow_data, local), + {:ok, activity} <- create_activity(unfollow_data, local), + {:ok, object} <- insert_full_object(unfollow_data), :ok <- maybe_federate(activity) do {:ok, activity, object} else @@ -331,7 +334,8 @@ defmodule Mobilizon.Service.ActivityPub do } with {:ok, _} <- Events.delete_event(event), - {:ok, activity, object} <- insert(data, local), + {:ok, activity} <- create_activity(data, local), + {:ok, object} <- insert_full_object(data), :ok <- maybe_federate(activity) do {:ok, activity, object} end @@ -347,7 +351,8 @@ defmodule Mobilizon.Service.ActivityPub do } with {:ok, _} <- Events.delete_comment(comment), - {:ok, activity, object} <- insert(data, local), + {:ok, activity} <- create_activity(data, local), + {:ok, object} <- insert_full_object(data), :ok <- maybe_federate(activity) do {:ok, activity, object} end @@ -363,7 +368,8 @@ defmodule Mobilizon.Service.ActivityPub do } with {:ok, _} <- Actors.delete_actor(actor), - {:ok, activity, object} <- insert(data, local), + {:ok, activity} <- create_activity(data, local), + {:ok, object} <- insert_full_object(data), :ok <- maybe_federate(activity) do {:ok, activity, object} end @@ -384,9 +390,10 @@ defmodule Mobilizon.Service.ActivityPub do end with flag_data <- make_flag_data(params, additional), - {:ok, activity, report} <- insert(flag_data, local), + {:ok, activity} <- create_activity(flag_data, local), + {:ok, object} <- insert_full_object(flag_data), :ok <- maybe_federate(activity) do - {:ok, activity, report} + {:ok, activity, object} end end @@ -403,7 +410,8 @@ defmodule Mobilizon.Service.ActivityPub do join_data <- Convertible.model_to_as(participant), join_data <- Map.put(join_data, "to", [event.organizer_actor.url]), join_data <- Map.put(join_data, "cc", []), - {:ok, activity, _} <- insert(join_data, local), + {:ok, activity} <- create_activity(join_data, local), + {:ok, _object} <- insert_full_object(join_data), :ok <- maybe_federate(activity) do if role === :participant do accept( @@ -443,7 +451,8 @@ defmodule Mobilizon.Service.ActivityPub do "to" => [event.organizer_actor.url], "cc" => [] }, - {:ok, activity, _} <- insert(leave_data, local), + {:ok, activity} <- create_activity(leave_data, local), + {:ok, _object} <- insert_full_object(leave_data), :ok <- maybe_federate(activity) do {:ok, activity, participant} end diff --git a/lib/service/activity_pub/converters/actor.ex b/lib/service/activity_pub/converters/actor.ex index 853173867..5e8c4ac3c 100644 --- a/lib/service/activity_pub/converters/actor.ex +++ b/lib/service/activity_pub/converters/actor.ex @@ -15,12 +15,30 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Actor do @impl Converter @spec as_to_model_data(map()) :: map() def as_to_model_data(object) do + avatar = + object["icon"]["url"] && + %{ + "name" => object["icon"]["name"] || "avatar", + "url" => object["icon"]["url"] + } + + banner = + object["image"]["url"] && + %{ + "name" => object["image"]["name"] || "banner", + "url" => object["image"]["url"] + } + %{ "type" => String.to_existing_atom(object["type"]), - "preferred_username" => object["preferred_username"], + "preferred_username" => object["preferredUsername"], "summary" => object["summary"], "url" => object["url"], - "name" => object["name"] + "name" => object["name"], + "avatar" => avatar, + "banner" => banner, + "keys" => object["publicKey"]["publicKeyPem"], + "manually_approves_followers" => object["manuallyApprovesFollowers"] } end diff --git a/lib/service/activity_pub/converters/event.ex b/lib/service/activity_pub/converters/event.ex index 0bfabd6aa..00f8cf0eb 100644 --- a/lib/service/activity_pub/converters/event.ex +++ b/lib/service/activity_pub/converters/event.ex @@ -57,6 +57,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do "organizer_actor_id" => actor_id, "picture_id" => picture_id, "begins_on" => object["startTime"], + "ends_on" => object["endTime"], "category" => object["category"], "url" => object["id"], "uuid" => object["uuid"], @@ -173,7 +174,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do "startTime" => event.begins_on |> date_to_string(), "endTime" => event.ends_on |> date_to_string(), "tag" => event.tags |> build_tags(), - "id" => event.url + "id" => event.url, + "url" => event.url } res = diff --git a/lib/service/activity_pub/transmogrifier.ex b/lib/service/activity_pub/transmogrifier.ex index a29b61ed5..2d5de4370 100644 --- a/lib/service/activity_pub/transmogrifier.ex +++ b/lib/service/activity_pub/transmogrifier.ex @@ -295,19 +295,15 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} = data ) - when object_type in ["Person", "Application", "Service", "Organization"] do + when object_type in ["Person", "Group", "Application", "Service", "Organization"] do case Actors.get_actor_by_url(object["id"]) do - {:ok, %Actor{url: url}} -> - {:ok, new_actor_data} = ActivityPub.actor_data_from_actor_object(object) - - Actors.insert_or_update_actor(new_actor_data) - + {:ok, %Actor{url: actor_url}} -> ActivityPub.update(%{ local: false, to: data["to"] || [], cc: data["cc"] || [], object: object, - actor: url + actor: actor_url }) e -> @@ -316,6 +312,28 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do end end + def handle_incoming( + %{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => actor} = + _update + ) do + with {:ok, %{"actor" => existing_organizer_actor_url} = _existing_event_data} <- + fetch_obj_helper_as_activity_streams(object), + {:ok, %Actor{url: actor_url}} <- actor |> Utils.get_url() |> Actors.get_actor_by_url(), + true <- Utils.get_url(existing_organizer_actor_url) == actor_url do + ActivityPub.update(%{ + local: false, + to: object["to"] || [], + cc: object["cc"] || [], + object: object, + actor: actor_url + }) + else + e -> + Logger.debug(inspect(e)) + :error + end + end + def handle_incoming( %{ "type" => "Undo", diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex index 31631c062..d68c62287 100644 --- a/lib/service/activity_pub/utils.ex +++ b/lib/service/activity_pub/utils.ex @@ -29,6 +29,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Endpoint + @actor_types ["Group", "Person", "Application"] + # Some implementations send the actor URI as the actor field, others send the entire actor object, # so figure out what the actor's URI is based on what we have. def get_url(%{"id" => id}), do: id @@ -119,7 +121,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do @doc """ Inserts a full object if it is contained in an activity. """ - def insert_full_object(%{"object" => %{"type" => "Event"} = object_data}) + def insert_full_object(%{"object" => %{"type" => "Event"} = object_data, "type" => "Create"}) when is_map(object_data) do with {:ok, object_data} <- Converters.Event.as_to_model_data(object_data), @@ -128,7 +130,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do end end - def insert_full_object(%{"object" => %{"type" => "Group"} = object_data}) + def insert_full_object(%{"object" => %{"type" => "Group"} = object_data, "type" => "Create"}) when is_map(object_data) do with object_data <- Map.put(object_data, "preferred_username", object_data["preferredUsername"]), @@ -140,7 +142,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do @doc """ Inserts a full object if it is contained in an activity. """ - def insert_full_object(%{"object" => %{"type" => "Note"} = object_data}) + def insert_full_object(%{"object" => %{"type" => "Note"} = object_data, "type" => "Create"}) when is_map(object_data) do with data <- Converters.Comment.as_to_model_data(object_data), {:ok, %Comment{} = comment} <- Events.create_comment(data) do @@ -177,6 +179,39 @@ defmodule Mobilizon.Service.ActivityPub.Utils do def insert_full_object(_), do: {:ok, nil} + @doc """ + Update an object + """ + @spec update_object(struct(), map()) :: {:ok, struct()} | any() + def update_object(object, object_data) + + def update_object(event_url, %{ + "object" => %{"type" => "Event"} = object_data, + "type" => "Update" + }) + when is_map(object_data) do + with {:event_not_found, %Event{} = event} <- + {:event_not_found, Events.get_event_by_url(event_url)}, + {:ok, object_data} <- Converters.Event.as_to_model_data(object_data), + {:ok, %Event{} = event} <- Events.update_event(event, object_data) do + {:ok, event} + end + end + + def update_object(actor_url, %{ + "object" => %{"type" => type_actor} = object_data, + "type" => "Update" + }) + when is_map(object_data) and type_actor in @actor_types do + with {:ok, %Actor{} = actor} <- Actors.get_actor_by_url(actor_url), + object_data <- Converters.Actor.as_to_model_data(object_data), + {:ok, %Actor{} = actor} <- Actors.update_actor(actor, object_data) do + {:ok, actor} + end + end + + def update_object(_, _), do: {:ok, nil} + #### Like-related helpers # @doc """ @@ -264,7 +299,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do String.t(), map(), list(), - map() + map(), + String.t() ) :: map() def make_event_data( actor, @@ -273,10 +309,12 @@ defmodule Mobilizon.Service.ActivityPub.Utils do content_html, picture \\ nil, tags \\ [], - metadata \\ %{} + metadata \\ %{}, + uuid \\ nil, + url \\ nil ) do Logger.debug("Making event data") - uuid = Ecto.UUID.generate() + uuid = uuid || Ecto.UUID.generate() res = %{ "type" => "Event", @@ -285,9 +323,10 @@ defmodule Mobilizon.Service.ActivityPub.Utils do "content" => content_html, "name" => title, "startTime" => metadata.begins_on, + "endTime" => metadata.ends_on, "category" => metadata.category, "actor" => actor, - "id" => Routes.page_url(Endpoint, :event, uuid), + "id" => url || Routes.page_url(Endpoint, :event, uuid), "uuid" => uuid, "tag" => tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end) @@ -505,7 +544,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do activity_id, public ) - when type in ["Group", "Person", "Application"] do + when type in @actor_types do do_make_announce_data(actor_url, actor_followers_url, url, url, activity_id, public) end diff --git a/schema.graphql b/schema.graphql index 8a8432422..a3ac11790 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,5 +1,5 @@ # source: http://localhost:4000/api -# timestamp: Mon Sep 02 2019 16:41:17 GMT+0200 (GMT+02:00) +# timestamp: Thu Sep 05 2019 13:00:10 GMT+0200 (GMT+02:00) schema { query: RootQueryType @@ -440,7 +440,7 @@ enum EventVisibility { """Visible only to people members of the group or followers of the person""" PRIVATE - """Publically listed and federated. Can be shared.""" + """Publicly listed and federated. Can be shared.""" PUBLIC """Visible only to people with the link - or invited""" @@ -823,7 +823,7 @@ type RootMutationType { """Create an event""" createEvent( beginsOn: DateTime! - category: String + category: String = "meeting" description: String! endsOn: DateTime onlineAddress: String @@ -852,11 +852,6 @@ type RootMutationType { """Create a group""" createGroup( - """ - The actor's username which will be the admin (otherwise user's default one) - """ - adminActorUsername: String - """ The avatar for the group, either as an object or directly the ID of an existing Picture """ @@ -867,14 +862,17 @@ type RootMutationType { """ banner: PictureInput - """The summary for the group""" - description: String = "" + """The identity that creates the group""" + creatorActorId: Int! """The displayed name for the group""" name: String """The name for the group""" preferredUsername: String! + + """The summary for the group""" + summary: String = "" ): Group """Create a new person for user""" @@ -969,6 +967,34 @@ type RootMutationType { """Send a link through email to reset user password""" sendResetPassword(email: String!, locale: String = "en"): String + """Update an event""" + updateEvent( + beginsOn: DateTime + category: String + description: String + endsOn: DateTime + eventId: ID! + onlineAddress: String + options: EventOptionsInput + organizerActorId: ID + phoneAddress: String + physicalAddress: AddressInput + + """ + The picture for the event, either as an object or directly the ID of an existing Picture + """ + picture: PictureInput + public: Boolean + publishAt: DateTime + state: Int + status: Int + + """The list of tags associated to the event""" + tags: [String] + title: String + visibility: EventVisibility + ): Event + """Update an identity""" updatePerson( """ diff --git a/test/fixtures/mastodon-update.json b/test/fixtures/mastodon-update.json index ab573c82a..c7d697a72 100644 --- a/test/fixtures/mastodon-update.json +++ b/test/fixtures/mastodon-update.json @@ -1,24 +1,24 @@ { "type": "Update", "object": { - "url": "http://mastodon.example.org/@gargron", + "url": "https://framapiaf.org/@framasoft", "type": "Person", "summary": "<p>Some bio</p>", "publicKey": { "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n", - "owner": "http://mastodon.example.org/users/gargron", - "id": "http://mastodon.example.org/users/gargron#main-key" + "owner": "https://framapiaf.org/users/framasoft", + "id": "https://framapiaf.org/users/framasoft#main-key" }, - "preferredUsername": "gargron", - "outbox": "http://mastodon.example.org/users/gargron/outbox", - "name": "gargle", + "preferredUsername": "framasoft", + "outbox": "https://framapiaf.org/users/framasoft/outbox", + "name": "nextsoft", "manuallyApprovesFollowers": false, - "inbox": "http://mastodon.example.org/users/gargron/inbox", - "id": "http://mastodon.example.org/users/gargron", - "following": "http://mastodon.example.org/users/gargron/following", - "followers": "http://mastodon.example.org/users/gargron/followers", + "inbox": "https://framapiaf.org/users/framasoft/inbox", + "id": "https://framapiaf.org/users/framasoft", + "following": "https://framapiaf.org/users/framasoft/following", + "followers": "https://framapiaf.org/users/framasoft/followers", "endpoints": { - "sharedInbox": "http://mastodon.example.org/inbox" + "sharedInbox": "https://framapiaf.org/inbox" }, "icon":{ "type":"Image", @@ -31,8 +31,8 @@ "url":"https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png" } }, - "id": "http://mastodon.example.org/users/gargron#updates/1519563538", - "actor": "http://mastodon.example.org/users/gargron", + "id": "https://framapiaf.org/users/gargron#updates/1519563538", + "actor": "https://framapiaf.org/users/framasoft", "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs index bcb320724..6b2ae6917 100644 --- a/test/mobilizon/actors/actors_test.exs +++ b/test/mobilizon/actors/actors_test.exs @@ -270,10 +270,8 @@ defmodule Mobilizon.ActorsTest do assert %Actor{} = actor assert actor.summary == "some updated description" assert actor.name == "some updated name" - assert actor.domain == "some updated domain" assert actor.keys == "some updated keys" refute actor.suspended - assert actor.preferred_username == "some updated username" end test "update_actor/2 with valid data updates the actor and it's media files", %{ @@ -310,10 +308,8 @@ defmodule Mobilizon.ActorsTest do assert %Actor{} = actor assert actor.summary == "some updated description" assert actor.name == "some updated name" - assert actor.domain == "some updated domain" assert actor.keys == "some updated keys" refute actor.suspended - assert actor.preferred_username == "some updated username" refute File.exists?( Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> diff --git a/test/mobilizon/service/activity_pub/activity_pub_test.exs b/test/mobilizon/service/activity_pub/activity_pub_test.exs index 638ed0a18..c744d3d68 100644 --- a/test/mobilizon/service/activity_pub/activity_pub_test.exs +++ b/test/mobilizon/service/activity_pub/activity_pub_test.exs @@ -9,6 +9,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do import Mobilizon.Factory alias Mobilizon.Events + alias Mobilizon.Events.Event alias Mobilizon.Actors.Actor alias Mobilizon.Actors alias Mobilizon.Service.HTTPSignatures.Signature @@ -137,11 +138,14 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do end describe "update" do + @updated_actor_summary "This is an updated actor" + test "it creates an update activity with the new actor data" do actor = insert(:actor) actor_data = MobilizonWeb.ActivityPub.ActorView.render("actor.json", %{actor: actor}) + actor_data = Map.put(actor_data, "summary", @updated_actor_summary) - {:ok, update, _} = + {:ok, update, updated_actor} = ActivityPub.update(%{ actor: actor_data["url"], to: [actor.url <> "/followers"], @@ -153,6 +157,42 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do assert update.data["to"] == [actor.url <> "/followers"] assert update.data["object"]["id"] == actor_data["id"] assert update.data["object"]["type"] == actor_data["type"] + assert update.data["object"]["summary"] == @updated_actor_summary + + refute updated_actor.summary == actor.summary + + {:ok, %Actor{} = database_actor} = Mobilizon.Actors.get_actor_by_url(actor.url) + assert database_actor.summary == @updated_actor_summary + assert database_actor.preferred_username == actor.preferred_username + end + + @updated_start_time DateTime.utc_now() |> DateTime.truncate(:second) + + test "it creates an update activity with the new event data" do + actor = insert(:actor) + event = insert(:event, organizer_actor: actor) + event_data = Mobilizon.Service.ActivityPub.Converters.Event.model_to_as(event) + event_data = Map.put(event_data, "startTime", @updated_start_time) + + {:ok, update, updated_event} = + ActivityPub.update(%{ + actor: actor.url, + to: [actor.url <> "/followers"], + cc: [], + object: event_data + }) + + assert update.data["actor"] == actor.url + assert update.data["to"] == [actor.url <> "/followers"] + assert update.data["object"]["id"] == event_data["id"] + assert update.data["object"]["type"] == event_data["type"] + assert update.data["object"]["startTime"] == @updated_start_time + + refute updated_event.begins_on == event.begins_on + + %Event{} = database_event = Mobilizon.Events.get_event_by_url(event.url) + assert database_event.begins_on == @updated_start_time + assert database_event.title == event.title end end end diff --git a/test/mobilizon/service/activity_pub/converters/actor_test.exs b/test/mobilizon/service/activity_pub/converters/actor_test.exs index e0b9fefb7..242fc9385 100644 --- a/test/mobilizon/service/activity_pub/converters/actor_test.exs +++ b/test/mobilizon/service/activity_pub/converters/actor_test.exs @@ -17,7 +17,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.ActorTest do actor = ActorConverter.as_to_model_data(%{ "type" => "Person", - "preferred_username" => "test_account" + "preferredUsername" => "test_account" }) assert actor["type"] == :Person diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs index dde9e57b5..cd636a3d9 100644 --- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs +++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs @@ -330,7 +330,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert data["object"] == comment.url end - test "it works for incoming update activities" do + test "it works for incoming update activities on actors" do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) @@ -349,11 +349,37 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(update_data) {:ok, %Actor{} = actor} = Actors.get_actor_by_url(data["actor"]) - assert actor.name == "gargle" + assert actor.name == "nextsoft" assert actor.summary == "<p>Some bio</p>" end + test "it works for incoming update activities on events" do + data = File.read!("test/fixtures/mobilizon-post-activity.json") |> Jason.decode!() + + {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) + update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!() + + object = + data["object"] + |> Map.put("actor", data["actor"]) + |> Map.put("name", "My updated event") + |> Map.put("id", data["object"]["id"]) + |> Map.put("type", "Event") + + update_data = + update_data + |> Map.put("actor", data["actor"]) + |> Map.put("object", object) + + {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(update_data) + + %Event{} = event = Events.get_event_by_url(data["object"]["id"]) + assert event.title == "My updated event" + + assert event.description == data["object"]["content"] + end + # test "it works for incoming update activities which lock the account" do # data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() diff --git a/test/mobilizon_web/resolvers/event_resolver_test.exs b/test/mobilizon_web/resolvers/event_resolver_test.exs index 077f2ec05..c01f9d0fd 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,166 @@ 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", + event_id: #{event.id} + ) { + title, + uuid, + tags { + title, + slug + } + } + } + """ + + res = + conn + |> auth_conn(user) + |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) + + assert hd(json_response(res, 200)["errors"])["message"] == "User doesn't own actor" + end + + test "update_event/3 updates an event", %{conn: conn, actor: actor, user: user} do + event = insert(:event, organizer_actor: actor) + + 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}", + event_id: #{event.id}, + organizer_actor_id: "#{actor.id}", + category: "birthday", + tags: ["tag1_updated", "tag2_updated"] + ) { + title, + uuid, + url, + 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"]["updateEvent"]["uuid"] == event.uuid + assert json_response(res, 200)["data"]["updateEvent"]["url"] == event.url + + assert json_response(res, 200)["data"]["updateEvent"]["tags"] == [ + %{"slug" => "tag1-updated", "title" => "tag1_updated"}, + %{"slug" => "tag2-updated", "title" => "tag2_updated"} + ] + end + + test "update_event/3 updates an event with a new picture", %{ + conn: conn, + actor: actor, + user: user + } do + event = insert(:event, organizer_actor: actor) + + 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}", + event_id: #{event.id}, + organizer_actor_id: "#{actor.id}", + category: "birthday", + picture: { + picture: { + name: "picture for my event", + alt: "A very sunny landscape", + file: "event.jpg", + actor_id: "#{actor.id}" + } + } + ) { + title, + uuid, + url, + picture { + name, + url + } + } + } + """ + + map = %{ + "query" => mutation, + "event.jpg" => %Plug.Upload{ + path: "test/fixtures/picture.png", + filename: "event.jpg" + } + } + + res = + conn + |> auth_conn(user) + |> put_req_header("content-type", "multipart/form-data") + |> post("/api", map) + + assert json_response(res, 200)["errors"] == nil + assert json_response(res, 200)["data"]["updateEvent"]["title"] == "my event updated" + assert json_response(res, 200)["data"]["updateEvent"]["uuid"] == event.uuid + assert json_response(res, 200)["data"]["updateEvent"]["url"] == event.url + + assert json_response(res, 200)["data"]["updateEvent"]["picture"]["name"] == + "picture for my event" + end + test "list_events/3 returns events", context do event = insert(:event) diff --git a/test/support/factory.ex b/test/support/factory.ex index eee37f91f..e91cd42a5 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -67,8 +67,8 @@ defmodule Mobilizon.Factory do def tag_factory do %Mobilizon.Events.Tag{ - title: "MyTag", - slug: sequence("MyTag") + title: sequence("MyTag"), + slug: sequence("my-tag") } end