Correctly handle event update

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2019-09-04 18:24:31 +02:00
parent 6845825db2
commit f5c3dbf128
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
27 changed files with 493 additions and 161 deletions

View file

@ -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) {

View file

@ -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;
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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'];
}

View file

@ -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>

View file

@ -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
"""

View file

@ -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

View file

@ -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}

View file

@ -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

View file

@ -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
@ -16,13 +15,13 @@ defmodule MobilizonWeb.API.Events do
with %{
title: title,
physical_address: physical_address,
visibility: visibility,
picture: picture,
content_html: content_html,
tags: tags,
to: to,
cc: cc,
begins_on: begins_on,
ends_on: ends_on,
category: category,
options: options
} <- prepare_args(args),
@ -34,7 +33,13 @@ defmodule MobilizonWeb.API.Events do
content_html,
picture,
tags,
%{begins_on: begins_on, physical_address: physical_address, category: category, options: options}
%{
begins_on: begins_on,
ends_on: ends_on,
physical_address: physical_address,
category: category,
options: options
}
) do
ActivityPub.create(%{
to: ["https://www.w3.org/ns/activitystreams#Public"],
@ -48,30 +53,28 @@ defmodule MobilizonWeb.API.Events do
@doc """
Update an event
"""
@spec update_event(map()) :: {:ok, Activity.t(), Event.t()} | any()
@spec update_event(map(), Event.t()) :: {:ok, Activity.t(), Event.t()} | any()
def update_event(
%{
organizer_actor: organizer_actor,
event: event
} = args
organizer_actor: organizer_actor
} = args,
%Event{} = event
) do
with %{
with args <- Map.put(args, :tags, Map.get(args, :tags, [])),
%{
title: title,
physical_address: physical_address,
visibility: visibility,
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
|> update_args(event)
),
prepare_args(Map.merge(event, args)),
event <-
ActivityPubUtils.make_event_data(
organizer_actor.url,
@ -82,34 +85,24 @@ 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
}
},
event.uuid,
event.url
) do
ActivityPub.update(%{
to: ["https://www.w3.org/ns/activitystreams#Public"],
actor: organizer_actor,
actor: organizer_actor.url,
cc: [],
object: event,
local: true
})
end
end
defp update_args(args, event) do
%{
title: Map.get(args, :title, event.title),
description: Map.get(args, :description, event.description),
tags: Map.get(args, :tags, event.tags),
physical_address: Map.get(args, :physical_address, event.physical_address),
visibility: Map.get(args, :visibility, event.visibility),
physical_address: Map.get(args, :physical_address, event.physical_address),
begins_on: Map.get(args, :begins_on, event.begins_on),
category: Map.get(args, :category, event.category),
options: Map.get(args, :options, event.options)
}
end
defp prepare_args(
%{
organizer_actor: organizer_actor,
@ -118,8 +111,7 @@ defmodule MobilizonWeb.API.Events do
options: options,
tags: tags,
begins_on: begins_on,
category: category,
options: options
category: category
} = args
) do
with physical_address <- Map.get(args, :physical_address, nil),
@ -131,13 +123,13 @@ defmodule MobilizonWeb.API.Events do
%{
title: title,
physical_address: physical_address,
visibility: visibility,
picture: picture,
content_html: content_html,
tags: tags,
to: to,
cc: cc,
begins_on: begins_on,
ends_on: Map.get(args, :ends_on, nil),
category: category,
options: options
}

View file

@ -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),

View file

@ -193,7 +193,9 @@ defmodule MobilizonWeb.Resolvers.Event do
}
} = _resolution
) do
with {:is_owned, true, organizer_actor} <- User.owns_actor(user, organizer_actor_id),
# 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),
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
@ -230,10 +232,13 @@ defmodule MobilizonWeb.Resolvers.Event do
}
} = _resolution
) do
with {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id),
# 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{
@ -243,11 +248,14 @@ defmodule MobilizonWeb.Resolvers.Event do
},
%Event{} = event
} <-
MobilizonWeb.API.Events.update_event(args) do
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

View file

@ -229,14 +229,14 @@ 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(:integer))
arg(:event_id, non_null(:id))
arg(:title, :string)
arg(:description, :string)
@ -246,6 +246,7 @@ defmodule MobilizonWeb.Schema.EventType do
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")
@ -259,6 +260,7 @@ defmodule MobilizonWeb.Schema.EventType do
arg(:phone_address, :string)
arg(:category, :string)
arg(:physical_address, :address_input)
arg(:options, :event_options_input)
resolve(&Event.update_event/3)
end

View file

@ -39,21 +39,16 @@ defmodule Mobilizon.Service.ActivityPub do
@doc """
Wraps an object into an activity
"""
@spec create_activity(map(), boolean()) :: {:ok, %Activity{}} | {:error, any()}
@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
activity = %Activity{
data: map,
local: local,
actor: map["actor"],
recipients: get_recipients(map)
}
# Notification.create_notifications(activity)
# stream_out(activity)
{:ok, activity}
else
error -> {:error, error}
{:ok,
%Activity{
data: map,
local: local,
actor: map["actor"],
recipients: get_recipients(map)
}}
end
end
@ -196,7 +191,7 @@ defmodule Mobilizon.Service.ActivityPub do
"object" => object
},
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
{:ok, object} <- update_object(object["id"], data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
end

View file

@ -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

View file

@ -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 =

View file

@ -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",

View file

@ -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

View file

@ -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(
"""

View file

@ -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",

View file

@ -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]) <>

View file

@ -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

View file

@ -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

View file

@ -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!()

View file

@ -418,12 +418,12 @@ 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
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: "my event updated"
) {
title,
uuid,
@ -445,7 +445,7 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
test "update_event/3 should check the user is an administrator", %{
conn: conn,
actor: actor,
actor: _actor,
user: user
} do
event = insert(:event)
@ -454,6 +454,7 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
mutation {
updateEvent(
title: "my event updated",
event_id: #{event.id}
) {
title,
uuid,
@ -470,11 +471,11 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] == nil
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)
event = insert(:event, organizer_actor: actor)
begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
@ -484,12 +485,14 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
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
@ -505,13 +508,76 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
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"]["createEvent"]["tags"] == [
%{"slug" => "tag1_updated", "title" => "tag1_updated"},
%{"slug" => "tag2_updated", "title" => "tag2_updated"}
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)

View file

@ -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