Merge branch 'feature/edit-event' into 'master'

Improve create event and prepare update event

See merge request framasoft/mobilizon!176
This commit is contained in:
Thomas Citharel 2019-09-05 14:09:09 +02:00
commit 060f6c8775
27 changed files with 856 additions and 173 deletions

View File

@ -81,11 +81,12 @@ import { Modal } from 'buefy/dist/components/dialog';
export default class AddressAutoComplete extends Vue { export default class AddressAutoComplete extends Vue {
@Prop({ required: false, default: () => [] }) initialData!: IAddress[]; @Prop({ required: false, default: () => [] }) initialData!: IAddress[];
@Prop({ required: false }) value!: IAddress;
data: IAddress[] = this.initialData; data: IAddress[] = this.initialData;
selected: IAddress|null = new Address(); selected: IAddress|null = new Address();
isFetching: boolean = false; isFetching: boolean = false;
queryText: string = ''; queryText: string = this.value && this.value.description || '';
addressModalActive: boolean = false; addressModalActive: boolean = false;
async getAsyncData(query) { async getAsyncData(query) {

View File

@ -1,33 +1,51 @@
<template> <template>
<b-field label="Enter some tags"> <b-field label="Enter some tags">
<b-taginput <b-taginput
v-model="tags" v-model="tagsStrings"
:data="filteredTags" :data="filteredTags"
autocomplete autocomplete
:allow-new="true" :allow-new="true"
:field="path" :field="path"
icon="label" icon="label"
placeholder="Add a tag" placeholder="Add a tag"
@typing="getFilteredTags"> @typing="getFilteredTags"
>
</b-taginput> </b-taginput>
</b-field> </b-field>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { get } from 'lodash'; import { get, differenceBy } from 'lodash';
import { ITag } from '@/types/tag.model'; 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 { 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, default: 'value' }) path!: string;
@Prop({ required: true }) value!: string; @Prop({ required: true }) value!: ITag[];
filteredTags: object[] = []; filteredTags: ITag[] = [];
tags: object[] = [];
getFilteredTags(text) { getFilteredTags(text) {
this.filteredTags = this.data.filter((option) => { this.filteredTags = differenceBy(this.data, this.value, 'id').filter((option) => {
return get(option, this.path) return get(option, this.path)
.toString() .toString()
.toLowerCase() .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 { static isTag(x: any): x is ITag {
return x.slug !== undefined; return x.slug !== undefined;
} }

View File

@ -52,6 +52,7 @@ export const FETCH_EVENT = gql`
domain, domain,
name, name,
url, url,
id,
}, },
# attributedTo { # attributedTo {
# avatar { # avatar {
@ -64,6 +65,7 @@ export const FETCH_EVENT = gql`
${participantQuery} ${participantQuery}
}, },
tags { tags {
id,
slug, slug,
title title
}, },
@ -82,6 +84,25 @@ export const FETCH_EVENT = gql`
domain, domain,
name, 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!, $organizerActorId: ID!,
$category: String, $category: String,
$beginsOn: DateTime!, $beginsOn: DateTime!,
$endsOn: DateTime,
$picture: PictureInput, $picture: PictureInput,
$tags: [String], $tags: [String],
$options: EventOptionsInput, $options: EventOptionsInput,
@ -154,6 +176,7 @@ export const CREATE_EVENT = gql`
title: $title, title: $title,
description: $description, description: $description,
beginsOn: $beginsOn, beginsOn: $beginsOn,
endsOn: $endsOn,
organizerActorId: $organizerActorId, organizerActorId: $organizerActorId,
category: $category, category: $category,
options: $options, options: $options,
@ -173,13 +196,32 @@ export const CREATE_EVENT = gql`
`; `;
export const EDIT_EVENT = gql` export const EDIT_EVENT = gql`
mutation EditEvent( mutation updateEvent(
$id: ID!,
$title: String!, $title: String!,
$description: String!, $description: String!,
$organizerActorId: Int!, $organizerActorId: ID!,
$category: String $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 uuid
} }
} }

View File

@ -4,9 +4,9 @@ import { ITag } from '@/types/tag.model';
import { IPicture } from '@/types/picture.model'; import { IPicture } from '@/types/picture.model';
export enum EventStatus { export enum EventStatus {
TENTATIVE, TENTATIVE = 'TENTATIVE',
CONFIRMED, CONFIRMED = 'CONFIRMED',
CANCELLED, CANCELLED = 'CANCELLED',
} }
export enum EventVisibility { export enum EventVisibility {
@ -17,9 +17,9 @@ export enum EventVisibility {
} }
export enum EventJoinOptions { export enum EventJoinOptions {
FREE, FREE = 'FREE',
RESTRICTED, RESTRICTED = 'RESTRICTED',
INVITE, INVITE = 'INVITE',
} }
export enum EventVisibilityJoinOptions { export enum EventVisibilityJoinOptions {

View File

@ -177,7 +177,7 @@
<script lang="ts"> <script lang="ts">
import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event'; import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; 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 { LOGGED_PERSON } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor'; import { IPerson, Person } from '@/types/actor';
import PictureUpload from '@/components/PictureUpload.vue'; import PictureUpload from '@/components/PictureUpload.vue';
@ -207,6 +207,7 @@ export default class EditEvent extends Vue {
eventId!: string | undefined; eventId!: string | undefined;
loggedPerson = new Person(); loggedPerson = new Person();
tags: ITag[] = [];
event = new EventModel(); event = new EventModel();
pictureFile: File | null = null; pictureFile: File | null = null;
@ -223,7 +224,7 @@ export default class EditEvent extends Vue {
@Watch('$route.params.eventId', { immediate: true }) @Watch('$route.params.eventId', { immediate: true })
async onEventIdParamChanged (val: string) { async onEventIdParamChanged (val: string) {
if (this.isUpdate !== true) return; if (!this.isUpdate) return;
this.eventId = val; this.eventId = val;
@ -231,6 +232,7 @@ export default class EditEvent extends Vue {
this.event = await this.getEvent(); this.event = await this.getEvent();
this.pictureFile = await buildFileFromIPicture(this.event.picture); 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.beginsOn = now;
this.event.endsOn = end; this.event.endsOn = end;
console.log('eventvisibilityjoinoptions', this.eventVisibilityJoinOptions);
} }
createOrUpdate(e: Event) { createOrUpdate(e: Event) {
@ -261,7 +262,7 @@ export default class EditEvent extends Vue {
console.log('Event created', data); console.log('Event created', data);
this.$router.push({ await this.$router.push({
name: 'Event', name: 'Event',
params: { uuid: data.createEvent.uuid }, params: { uuid: data.createEvent.uuid },
}); });
@ -277,7 +278,7 @@ export default class EditEvent extends Vue {
variables: this.buildVariables(), variables: this.buildVariables(),
}); });
this.$router.push({ await this.$router.push({
name: 'Event', name: 'Event',
params: { uuid: this.eventId as string }, params: { uuid: this.eventId as string },
}); });
@ -297,6 +298,8 @@ export default class EditEvent extends Vue {
}; };
const res = Object.assign({}, this.event, obj); const res = Object.assign({}, this.event, obj);
delete this.event.options['__typename'];
if (this.event.physicalAddress) { if (this.event.physicalAddress) {
delete this.event.physicalAddress['__typename']; delete this.event.physicalAddress['__typename'];
} }

View File

@ -57,7 +57,7 @@
<p class="control"> <p class="control">
<router-link <router-link
class="button" class="button"
:to="{ name: 'EditEvent', params: {uuid: event.uuid}}" :to="{ name: 'EditEvent', params: {eventId: event.uuid}}"
> >
<translate>Edit</translate> <translate>Edit</translate>
</router-link> </router-link>

View File

@ -110,6 +110,24 @@ defmodule Mobilizon.Actors.Actor do
|> unique_constraint(:url, name: :actors_url_index) |> unique_constraint(:url, name: :actors_url_index)
end 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 """ @doc """
Changeset for person registration Changeset for person registration
""" """

View File

@ -124,7 +124,7 @@ defmodule Mobilizon.Actors do
""" """
def update_actor(%Actor{} = actor, attrs) do def update_actor(%Actor{} = actor, attrs) do
actor actor
|> Actor.changeset(attrs) |> Actor.update_changeset(attrs)
|> delete_files_if_media_changed() |> delete_files_if_media_changed()
|> Repo.update() |> Repo.update()
end end

View File

@ -54,10 +54,10 @@ defmodule Mobilizon.Events.Event do
field(:online_address, :string) field(:online_address, :string)
field(:phone_address, :string) field(:phone_address, :string)
field(:category, :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(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_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) many_to_many(:participants, Actor, join_through: Participant)
has_many(:tracks, Track) has_many(:tracks, Track)
has_many(:sessions, Session) has_many(:sessions, Session)
@ -98,6 +98,38 @@ defmodule Mobilizon.Events.Event do
]) ])
end 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) def can_event_be_managed_by(%Event{organizer_actor_id: organizer_actor_id}, actor_id)
when organizer_actor_id == actor_id do when organizer_actor_id == actor_id do
{:event_can_be_managed, true} {:event_can_be_managed, true}

View File

@ -227,11 +227,12 @@ defmodule Mobilizon.Events do
:tracks, :tracks,
:tags, :tags,
:participants, :participants,
:physical_address :physical_address,
:picture
])} ])}
err -> _err ->
{:error, err} {:error, :event_not_found}
end end
end end
@ -435,7 +436,8 @@ defmodule Mobilizon.Events do
""" """
def update_event(%Event{} = event, attrs) do def update_event(%Event{} = event, attrs) do
event event
|> Event.changeset(attrs) |> Repo.preload(:tags)
|> Event.update_changeset(attrs)
|> Repo.update() |> Repo.update()
end end

View File

@ -2,8 +2,7 @@ defmodule MobilizonWeb.API.Events do
@moduledoc """ @moduledoc """
API for Events API for Events
""" """
alias Mobilizon.Actors alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias MobilizonWeb.API.Utils alias MobilizonWeb.API.Utils
@ -12,28 +11,23 @@ defmodule MobilizonWeb.API.Events do
Create an event Create an event
""" """
@spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any() @spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any()
def create_event( def create_event(%{organizer_actor: organizer_actor} = args) do
%{ with %{
begins_on: begins_on, title: title,
description: description, physical_address: physical_address,
options: options, picture: picture,
organizer_actor_id: organizer_actor_id, content_html: content_html,
tags: tags, tags: tags,
title: title to: to,
} = args cc: cc,
) begins_on: begins_on,
when is_map(options) do ends_on: ends_on,
with %Actor{url: url} = actor <- category: category,
Actors.get_local_actor_with_everything(organizer_actor_id), options: options
physical_address <- Map.get(args, :physical_address, nil), } <- prepare_args(args),
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),
event <- event <-
ActivityPubUtils.make_event_data( ActivityPubUtils.make_event_data(
url, organizer_actor.url,
%{to: to, cc: cc}, %{to: to, cc: cc},
title, title,
content_html, content_html,
@ -41,17 +35,104 @@ defmodule MobilizonWeb.API.Events do
tags, tags,
%{ %{
begins_on: begins_on, begins_on: begins_on,
ends_on: ends_on,
physical_address: physical_address, physical_address: physical_address,
category: Map.get(args, :category), category: category,
options: options options: options
} }
) do ) do
ActivityPub.create(%{ ActivityPub.create(%{
to: ["https://www.w3.org/ns/activitystreams#Public"], to: ["https://www.w3.org/ns/activitystreams#Public"],
actor: actor, actor: organizer_actor,
object: event, object: event,
local: true local: true
}) })
end end
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 end

View File

@ -18,13 +18,13 @@ defmodule MobilizonWeb.API.Groups do
preferred_username: title, preferred_username: title,
summary: summary, summary: summary,
creator_actor_id: creator_actor_id, creator_actor_id: creator_actor_id,
avatar: avatar, avatar: _avatar,
banner: banner banner: _banner
} = args } = args
) do ) do
with {:is_owned, true, actor} <- User.owns_actor(user, creator_actor_id), 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), title <- String.trim(title),
{:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
visibility <- Map.get(args, :visibility, :public), visibility <- Map.get(args, :visibility, :public),
{content_html, tags, to, cc} <- {content_html, tags, to, cc} <-
Utils.prepare_content(actor, summary, visibility, [], nil), Utils.prepare_content(actor, summary, visibility, [], nil),

View File

@ -58,7 +58,8 @@ defmodule MobilizonWeb.Resolvers.Event do
) do ) do
# We get the organizer's next public event # We get the organizer's next public event
events = 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 # We find similar events with the same tags
# uniq_by : It's possible event_from_same_actor is inside events_from_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, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_full(event_id)}, {:has_event, Mobilizon.Events.get_event_full(event_id)},
{:ok, _activity, _participant} <- MobilizonWeb.API.Participations.leave(event, actor) do {: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 else
{:has_event, _} -> {:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"} {:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
@ -173,12 +184,35 @@ defmodule MobilizonWeb.Resolvers.Event do
@doc """ @doc """
Create an event Create an event
""" """
def create_event(_parent, args, %{context: %{current_user: _user}} = _resolution) do def create_event(
with {:ok, args} <- save_attached_picture(args), _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, args} <- save_physical_address(args),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"} = _object}}, %Event{} = event} <- args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
MobilizonWeb.API.Events.create_event(args) do {
:ok,
%Activity{
data: %{
"object" => %{"type" => "Event"} = _object
}
},
%Event{} = event
} <-
MobilizonWeb.API.Events.create_event(args_with_organizer) do
{:ok, event} {:ok, event}
else
{:is_owned, false} ->
{:error, "Organizer actor id is not owned by the user"}
end end
end end
@ -186,19 +220,72 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, "You need to be logged-in to create events"} {:error, "You need to be logged-in to create events"}
end 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 # If we have an attached picture, just transmit it. It will be handled by
# Mobilizon.Service.ActivityPub.Utils.make_picture_data/1 # Mobilizon.Service.ActivityPub.Utils.make_picture_data/1
# However, we need to pass it's actor ID # However, we need to pass it's actor ID
@spec save_attached_picture(map()) :: {:ok, map()} @spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture( defp save_attached_picture(
%{picture: %{picture: %{file: %Plug.Upload{} = _picture} = all_pic}} = args %{
picture: %{
picture: %{file: %Plug.Upload{} = _picture} = all_pic
}
} = args
) do ) do
{:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor_id))} {:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor_id))}
end end
# Otherwise if we use a previously uploaded picture we need to fetch it from database # Otherwise if we use a previously uploaded picture we need to fetch it from database
@spec save_attached_picture(map()) :: {:ok, map()} @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 with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do
{:ok, Map.put(args, :picture, picture)} {:ok, Map.put(args, :picture, picture)}
end end
@ -208,7 +295,13 @@ defmodule MobilizonWeb.Resolvers.Event do
defp save_attached_picture(args), do: {:ok, args} defp save_attached_picture(args), do: {:ok, args}
@spec save_physical_address(map()) :: {:ok, map()} @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 when not is_nil(physical_address_url) do
with %Address{} = address <- Addresses.get_address_by_url(physical_address_url), with %Address{} = address <- Addresses.get_address_by_url(physical_address_url),
args <- Map.put(args, :physical_address, address.url) do args <- Map.put(args, :physical_address, address.url) do
@ -230,9 +323,15 @@ defmodule MobilizonWeb.Resolvers.Event do
@doc """ @doc """
Delete an event Delete an event
""" """
def delete_event(_parent, %{event_id: event_id, actor_id: actor_id}, %{ def delete_event(
context: %{current_user: user} _parent,
}) do %{event_id: event_id, actor_id: actor_id},
%{
context: %{
current_user: user
}
}
) do
with {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id), 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_can_be_managed, true} <- Event.can_event_be_managed_by(event, actor_id),

View File

@ -229,11 +229,42 @@ defmodule MobilizonWeb.Schema.EventType do
arg(:organizer_actor_id, non_null(:id)) arg(:organizer_actor_id, non_null(:id))
arg(:category, :string, default_value: "meeting") arg(:category, :string, default_value: "meeting")
arg(:physical_address, :address_input) arg(:physical_address, :address_input)
arg(:options, :event_options_input, default_value: %{}) arg(:options, :event_options_input)
resolve(&Event.create_event/3) resolve(&Event.create_event/3)
end 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" @desc "Delete an event"
field :delete_event, :deleted_object do field :delete_event, :deleted_object do
arg(:event_id, non_null(:integer)) arg(:event_id, non_null(:integer))

View File

@ -39,24 +39,16 @@ defmodule Mobilizon.Service.ActivityPub do
@doc """ @doc """
Wraps an object into an activity Wraps an object into an activity
""" """
# TODO: Rename me @spec create_activity(map(), boolean()) :: {:ok, %Activity{}}
@spec insert(map(), boolean()) :: {:ok, %Activity{}} | {:error, any()} def create_activity(map, local \\ true) when is_map(map) do
def insert(map, local \\ true) when is_map(map) do with map <- lazy_put_activity_defaults(map) do
with map <- lazy_put_activity_defaults(map), {:ok,
{:ok, object} <- insert_full_object(map) do %Activity{
activity = %Activity{ data: map,
data: map, local: local,
local: local, actor: map["actor"],
actor: map["actor"], recipients: get_recipients(map)
recipients: get_recipients(map) }}
}
# Notification.create_notifications(activity)
# stream_out(activity)
{:ok, activity, object}
else
%Activity{} = activity -> {:ok, activity}
error -> {:error, error}
end end
end end
@ -137,7 +129,8 @@ defmodule Mobilizon.Service.ActivityPub do
%{to: to, actor: actor, published: published, object: object}, %{to: to, actor: actor, published: published, object: object},
additional 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 <- maybe_federate(activity) do
# {:ok, actor} <- Actors.increase_event_count(actor) do # {:ok, actor} <- Actors.increase_event_count(actor) do
{:ok, activity, object} {:ok, activity, object}
@ -160,7 +153,8 @@ defmodule Mobilizon.Service.ActivityPub do
"object" => object, "object" => object,
"id" => activity_wrapper_id || get_url(object) <> "/activity" "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 <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
end end
@ -177,7 +171,8 @@ defmodule Mobilizon.Service.ActivityPub do
"object" => object, "object" => object,
"id" => activity_wrapper_id || get_url(object) <> "/activity" "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 <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
end end
@ -195,7 +190,8 @@ defmodule Mobilizon.Service.ActivityPub do
"actor" => actor, "actor" => actor,
"object" => object "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 <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
end end
@ -210,7 +206,8 @@ defmodule Mobilizon.Service.ActivityPub do
# ) do # ) do
# with nil <- get_existing_like(url, object), # with nil <- get_existing_like(url, object),
# like_data <- make_like_data(user, object, activity_id), # 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, object} <- add_like_to_object(activity, object),
# :ok <- maybe_federate(activity) do # :ok <- maybe_federate(activity) do
# {:ok, activity, object} # {:ok, activity, object}
@ -228,7 +225,8 @@ defmodule Mobilizon.Service.ActivityPub do
# ) do # ) do
# with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object), # with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
# unlike_data <- make_unlike_data(actor, like_activity, activity_id), # 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, _activity} <- Repo.delete(like_activity),
# {:ok, object} <- remove_like_from_object(like_activity, object), # {:ok, object} <- remove_like_from_object(like_activity, object),
# :ok <- maybe_federate(unlike_activity) do # :ok <- maybe_federate(unlike_activity) do
@ -247,7 +245,8 @@ defmodule Mobilizon.Service.ActivityPub do
) do ) do
with true <- is_public?(object), with true <- is_public?(object),
announce_data <- make_announce_data(actor, object, activity_id, public), 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 <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
else else
@ -265,7 +264,8 @@ defmodule Mobilizon.Service.ActivityPub do
) do ) do
with announce_activity <- make_announce_data(actor, object, cancelled_activity_id), with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
unannounce_data <- make_unannounce_data(actor, announce_activity, 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 <- maybe_federate(unannounce_activity) do
{:ok, unannounce_activity, object} {:ok, unannounce_activity, object}
else else
@ -282,7 +282,8 @@ defmodule Mobilizon.Service.ActivityPub do
activity_follow_id <- activity_follow_id <-
activity_id || follow_url, activity_id || follow_url,
data <- make_follow_data(followed, follower, activity_follow_id), 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 <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
else else
@ -304,12 +305,14 @@ defmodule Mobilizon.Service.ActivityPub do
follower, follower,
"#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity" "#{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_unfollow_id <-
activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity", activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity",
unfollow_data <- unfollow_data <-
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id), 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 <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
else else
@ -331,7 +334,8 @@ defmodule Mobilizon.Service.ActivityPub do
} }
with {:ok, _} <- Events.delete_event(event), 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 <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
end end
@ -347,7 +351,8 @@ defmodule Mobilizon.Service.ActivityPub do
} }
with {:ok, _} <- Events.delete_comment(comment), 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 <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
end end
@ -363,7 +368,8 @@ defmodule Mobilizon.Service.ActivityPub do
} }
with {:ok, _} <- Actors.delete_actor(actor), 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 <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
end end
@ -384,9 +390,10 @@ defmodule Mobilizon.Service.ActivityPub do
end end
with flag_data <- make_flag_data(params, additional), 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 <- maybe_federate(activity) do
{:ok, activity, report} {:ok, activity, object}
end end
end end
@ -403,7 +410,8 @@ defmodule Mobilizon.Service.ActivityPub do
join_data <- Convertible.model_to_as(participant), join_data <- Convertible.model_to_as(participant),
join_data <- Map.put(join_data, "to", [event.organizer_actor.url]), join_data <- Map.put(join_data, "to", [event.organizer_actor.url]),
join_data <- Map.put(join_data, "cc", []), 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 :ok <- maybe_federate(activity) do
if role === :participant do if role === :participant do
accept( accept(
@ -443,7 +451,8 @@ defmodule Mobilizon.Service.ActivityPub do
"to" => [event.organizer_actor.url], "to" => [event.organizer_actor.url],
"cc" => [] "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 <- maybe_federate(activity) do
{:ok, activity, participant} {:ok, activity, participant}
end end

View File

@ -15,12 +15,30 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Actor do
@impl Converter @impl Converter
@spec as_to_model_data(map()) :: map() @spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do 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"]), "type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferred_username"], "preferred_username" => object["preferredUsername"],
"summary" => object["summary"], "summary" => object["summary"],
"url" => object["url"], "url" => object["url"],
"name" => object["name"] "name" => object["name"],
"avatar" => avatar,
"banner" => banner,
"keys" => object["publicKey"]["publicKeyPem"],
"manually_approves_followers" => object["manuallyApprovesFollowers"]
} }
end end

View File

@ -57,6 +57,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
"organizer_actor_id" => actor_id, "organizer_actor_id" => actor_id,
"picture_id" => picture_id, "picture_id" => picture_id,
"begins_on" => object["startTime"], "begins_on" => object["startTime"],
"ends_on" => object["endTime"],
"category" => object["category"], "category" => object["category"],
"url" => object["id"], "url" => object["id"],
"uuid" => object["uuid"], "uuid" => object["uuid"],
@ -173,7 +174,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
"startTime" => event.begins_on |> date_to_string(), "startTime" => event.begins_on |> date_to_string(),
"endTime" => event.ends_on |> date_to_string(), "endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> build_tags(), "tag" => event.tags |> build_tags(),
"id" => event.url "id" => event.url,
"url" => event.url
} }
res = res =

View File

@ -295,19 +295,15 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} = %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} =
data 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 case Actors.get_actor_by_url(object["id"]) do
{:ok, %Actor{url: url}} -> {:ok, %Actor{url: actor_url}} ->
{:ok, new_actor_data} = ActivityPub.actor_data_from_actor_object(object)
Actors.insert_or_update_actor(new_actor_data)
ActivityPub.update(%{ ActivityPub.update(%{
local: false, local: false,
to: data["to"] || [], to: data["to"] || [],
cc: data["cc"] || [], cc: data["cc"] || [],
object: object, object: object,
actor: url actor: actor_url
}) })
e -> e ->
@ -316,6 +312,28 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end end
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( def handle_incoming(
%{ %{
"type" => "Undo", "type" => "Undo",

View File

@ -29,6 +29,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint alias MobilizonWeb.Endpoint
@actor_types ["Group", "Person", "Application"]
# Some implementations send the actor URI as the actor field, others send the entire actor object, # 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. # so figure out what the actor's URI is based on what we have.
def get_url(%{"id" => id}), do: id def get_url(%{"id" => id}), do: id
@ -119,7 +121,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
@doc """ @doc """
Inserts a full object if it is contained in an activity. 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 when is_map(object_data) do
with {:ok, object_data} <- with {:ok, object_data} <-
Converters.Event.as_to_model_data(object_data), Converters.Event.as_to_model_data(object_data),
@ -128,7 +130,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
end end
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 when is_map(object_data) do
with object_data <- with object_data <-
Map.put(object_data, "preferred_username", object_data["preferredUsername"]), Map.put(object_data, "preferred_username", object_data["preferredUsername"]),
@ -140,7 +142,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
@doc """ @doc """
Inserts a full object if it is contained in an activity. 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 when is_map(object_data) do
with data <- Converters.Comment.as_to_model_data(object_data), with data <- Converters.Comment.as_to_model_data(object_data),
{:ok, %Comment{} = comment} <- Events.create_comment(data) do {: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} 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 #### Like-related helpers
# @doc """ # @doc """
@ -264,7 +299,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
String.t(), String.t(),
map(), map(),
list(), list(),
map() map(),
String.t()
) :: map() ) :: map()
def make_event_data( def make_event_data(
actor, actor,
@ -273,10 +309,12 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
content_html, content_html,
picture \\ nil, picture \\ nil,
tags \\ [], tags \\ [],
metadata \\ %{} metadata \\ %{},
uuid \\ nil,
url \\ nil
) do ) do
Logger.debug("Making event data") Logger.debug("Making event data")
uuid = Ecto.UUID.generate() uuid = uuid || Ecto.UUID.generate()
res = %{ res = %{
"type" => "Event", "type" => "Event",
@ -285,9 +323,10 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"content" => content_html, "content" => content_html,
"name" => title, "name" => title,
"startTime" => metadata.begins_on, "startTime" => metadata.begins_on,
"endTime" => metadata.ends_on,
"category" => metadata.category, "category" => metadata.category,
"actor" => actor, "actor" => actor,
"id" => Routes.page_url(Endpoint, :event, uuid), "id" => url || Routes.page_url(Endpoint, :event, uuid),
"uuid" => uuid, "uuid" => uuid,
"tag" => "tag" =>
tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end) tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end)
@ -505,7 +544,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
activity_id, activity_id,
public 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) do_make_announce_data(actor_url, actor_followers_url, url, url, activity_id, public)
end end

View File

@ -1,5 +1,5 @@
# source: http://localhost:4000/api # 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 { schema {
query: RootQueryType query: RootQueryType
@ -440,7 +440,7 @@ enum EventVisibility {
"""Visible only to people members of the group or followers of the person""" """Visible only to people members of the group or followers of the person"""
PRIVATE PRIVATE
"""Publically listed and federated. Can be shared.""" """Publicly listed and federated. Can be shared."""
PUBLIC PUBLIC
"""Visible only to people with the link - or invited""" """Visible only to people with the link - or invited"""
@ -823,7 +823,7 @@ type RootMutationType {
"""Create an event""" """Create an event"""
createEvent( createEvent(
beginsOn: DateTime! beginsOn: DateTime!
category: String category: String = "meeting"
description: String! description: String!
endsOn: DateTime endsOn: DateTime
onlineAddress: String onlineAddress: String
@ -852,11 +852,6 @@ type RootMutationType {
"""Create a group""" """Create a group"""
createGroup( 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 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 banner: PictureInput
"""The summary for the group""" """The identity that creates the group"""
description: String = "" creatorActorId: Int!
"""The displayed name for the group""" """The displayed name for the group"""
name: String name: String
"""The name for the group""" """The name for the group"""
preferredUsername: String! preferredUsername: String!
"""The summary for the group"""
summary: String = ""
): Group ): Group
"""Create a new person for user""" """Create a new person for user"""
@ -969,6 +967,34 @@ type RootMutationType {
"""Send a link through email to reset user password""" """Send a link through email to reset user password"""
sendResetPassword(email: String!, locale: String = "en"): String 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""" """Update an identity"""
updatePerson( updatePerson(
""" """

View File

@ -1,24 +1,24 @@
{ {
"type": "Update", "type": "Update",
"object": { "object": {
"url": "http://mastodon.example.org/@gargron", "url": "https://framapiaf.org/@framasoft",
"type": "Person", "type": "Person",
"summary": "<p>Some bio</p>", "summary": "<p>Some bio</p>",
"publicKey": { "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", "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", "owner": "https://framapiaf.org/users/framasoft",
"id": "http://mastodon.example.org/users/gargron#main-key" "id": "https://framapiaf.org/users/framasoft#main-key"
}, },
"preferredUsername": "gargron", "preferredUsername": "framasoft",
"outbox": "http://mastodon.example.org/users/gargron/outbox", "outbox": "https://framapiaf.org/users/framasoft/outbox",
"name": "gargle", "name": "nextsoft",
"manuallyApprovesFollowers": false, "manuallyApprovesFollowers": false,
"inbox": "http://mastodon.example.org/users/gargron/inbox", "inbox": "https://framapiaf.org/users/framasoft/inbox",
"id": "http://mastodon.example.org/users/gargron", "id": "https://framapiaf.org/users/framasoft",
"following": "http://mastodon.example.org/users/gargron/following", "following": "https://framapiaf.org/users/framasoft/following",
"followers": "http://mastodon.example.org/users/gargron/followers", "followers": "https://framapiaf.org/users/framasoft/followers",
"endpoints": { "endpoints": {
"sharedInbox": "http://mastodon.example.org/inbox" "sharedInbox": "https://framapiaf.org/inbox"
}, },
"icon":{ "icon":{
"type":"Image", "type":"Image",
@ -31,8 +31,8 @@
"url":"https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png" "url":"https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png"
} }
}, },
"id": "http://mastodon.example.org/users/gargron#updates/1519563538", "id": "https://framapiaf.org/users/gargron#updates/1519563538",
"actor": "http://mastodon.example.org/users/gargron", "actor": "https://framapiaf.org/users/framasoft",
"@context": [ "@context": [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1", "https://w3id.org/security/v1",

View File

@ -270,10 +270,8 @@ defmodule Mobilizon.ActorsTest do
assert %Actor{} = actor assert %Actor{} = actor
assert actor.summary == "some updated description" assert actor.summary == "some updated description"
assert actor.name == "some updated name" assert actor.name == "some updated name"
assert actor.domain == "some updated domain"
assert actor.keys == "some updated keys" assert actor.keys == "some updated keys"
refute actor.suspended refute actor.suspended
assert actor.preferred_username == "some updated username"
end end
test "update_actor/2 with valid data updates the actor and it's media files", %{ 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{} = actor
assert actor.summary == "some updated description" assert actor.summary == "some updated description"
assert actor.name == "some updated name" assert actor.name == "some updated name"
assert actor.domain == "some updated domain"
assert actor.keys == "some updated keys" assert actor.keys == "some updated keys"
refute actor.suspended refute actor.suspended
assert actor.preferred_username == "some updated username"
refute File.exists?( refute File.exists?(
Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <>

View File

@ -9,6 +9,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
import Mobilizon.Factory import Mobilizon.Factory
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Service.HTTPSignatures.Signature alias Mobilizon.Service.HTTPSignatures.Signature
@ -137,11 +138,14 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
end end
describe "update" do describe "update" do
@updated_actor_summary "This is an updated actor"
test "it creates an update activity with the new actor data" do test "it creates an update activity with the new actor data" do
actor = insert(:actor) actor = insert(:actor)
actor_data = MobilizonWeb.ActivityPub.ActorView.render("actor.json", %{actor: 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(%{ ActivityPub.update(%{
actor: actor_data["url"], actor: actor_data["url"],
to: [actor.url <> "/followers"], to: [actor.url <> "/followers"],
@ -153,6 +157,42 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
assert update.data["to"] == [actor.url <> "/followers"] assert update.data["to"] == [actor.url <> "/followers"]
assert update.data["object"]["id"] == actor_data["id"] assert update.data["object"]["id"] == actor_data["id"]
assert update.data["object"]["type"] == actor_data["type"] 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 end
end end

View File

@ -17,7 +17,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.ActorTest do
actor = actor =
ActorConverter.as_to_model_data(%{ ActorConverter.as_to_model_data(%{
"type" => "Person", "type" => "Person",
"preferred_username" => "test_account" "preferredUsername" => "test_account"
}) })
assert actor["type"] == :Person assert actor["type"] == :Person

View File

@ -330,7 +330,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert data["object"] == comment.url assert data["object"] == comment.url
end 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!() data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) {: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, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(update_data)
{:ok, %Actor{} = actor} = Actors.get_actor_by_url(data["actor"]) {: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>" assert actor.summary == "<p>Some bio</p>"
end 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 # test "it works for incoming update activities which lock the account" do
# data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() # data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()

View File

@ -58,6 +58,40 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
json_response(res, 200)["errors"] json_response(res, 200)["errors"]
end 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 test "create_event/3 creates an event", %{conn: conn, actor: actor, user: user} do
mutation = """ mutation = """
mutation { mutation {
@ -384,6 +418,166 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
assert json_response(res, 200)["data"]["createEvent"]["picture"]["url"] assert json_response(res, 200)["data"]["createEvent"]["picture"]["url"]
end 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 test "list_events/3 returns events", context do
event = insert(:event) event = insert(:event)

View File

@ -67,8 +67,8 @@ defmodule Mobilizon.Factory do
def tag_factory do def tag_factory do
%Mobilizon.Events.Tag{ %Mobilizon.Events.Tag{
title: "MyTag", title: sequence("MyTag"),
slug: sequence("MyTag") slug: sequence("my-tag")
} }
end end