wip events

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-07-22 23:40:53 +02:00
parent 0c9e64265c
commit 3a6274f29a
17 changed files with 544 additions and 26 deletions

View file

@ -19,7 +19,9 @@ defmodule Pleroma.Constants do
"context_id", "context_id",
"deleted_activity_id", "deleted_activity_id",
"pleroma_internal", "pleroma_internal",
"generator" "generator",
"participants",
"participant_count"
] ]
) )

View file

@ -19,6 +19,30 @@ defmodule Pleroma.Web.ActivityPub.Builder do
require Pleroma.Constants require Pleroma.Constants
def accept_or_reject(%User{ap_id: ap_id}, activity, type) do
data = %{
"id" => Utils.generate_activity_id(),
"actor" => ap_id,
"type" => type,
"object" => activity.data["id"],
"to" => [activity.actor]
}
{:ok, data, []}
end
def accept_or_reject(%Object{data: %{"actor" => actor}} = object, activity, type) do
data = %{
"id" => Utils.generate_activity_id(),
"actor" => actor,
"type" => type,
"object" => activity.data["id"],
"to" => [activity.actor]
}
{:ok, data, []}
end
def accept_or_reject(actor, activity, type) do def accept_or_reject(actor, activity, type) do
data = %{ data = %{
"id" => Utils.generate_activity_id(), "id" => Utils.generate_activity_id(),
@ -31,14 +55,14 @@ def accept_or_reject(actor, activity, type) do
{:ok, data, []} {:ok, data, []}
end end
@spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()} @spec reject(User.t() | Object.t(), Activity.t()) :: {:ok, map(), keyword()}
def reject(actor, rejected_activity) do def reject(object, rejected_activity) do
accept_or_reject(actor, rejected_activity, "Reject") accept_or_reject(object, rejected_activity, "Reject")
end end
@spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()} @spec accept(User.t() | Object.t(), Activity.t()) :: {:ok, map(), keyword()}
def accept(actor, accepted_activity) do def accept(object, accepted_activity) do
accept_or_reject(actor, accepted_activity, "Accept") accept_or_reject(object, accepted_activity, "Accept")
end end
@spec follow(User.t(), User.t()) :: {:ok, map(), keyword()} @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
@ -337,4 +361,14 @@ def unpin(%User{} = user, object) do
defp pinned_url(nickname) when is_binary(nickname) do defp pinned_url(nickname) when is_binary(nickname) do
Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname) Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
end end
def join(actor, object) do
with {:ok, data, meta} <- object_action(actor, object) do
data =
data
|> Map.put("type", "Join")
{:ok, data, meta}
end
end
end end

View file

@ -30,6 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EventValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EventValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.JoinValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
@ -143,7 +144,7 @@ def validate(%{"type" => type} = object, meta)
def validate(%{"type" => type} = object, meta) def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
ChatMessage Answer] do ChatMessage Answer Join] do
validator = validator =
case type do case type do
"Accept" -> AcceptRejectValidator "Accept" -> AcceptRejectValidator
@ -155,6 +156,7 @@ def validate(%{"type" => type} = object, meta)
"Announce" -> AnnounceValidator "Announce" -> AnnounceValidator
"ChatMessage" -> ChatMessageValidator "ChatMessage" -> ChatMessageValidator
"Answer" -> AnswerValidator "Answer" -> AnswerValidator
"Join" -> JoinValidator
end end
with {:ok, object} <- with {:ok, object} <-

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object
import Ecto.Changeset import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@ -32,7 +33,7 @@ defp validate_data(cng) do
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Accept", "Reject"]) |> validate_inclusion(:type, ["Accept", "Reject"])
|> validate_actor_presence() |> validate_actor_presence()
|> validate_object_presence(allowed_types: ["Follow"]) |> validate_object_presence(allowed_types: ["Follow", "Join"])
|> validate_accept_reject_rights() |> validate_accept_reject_rights()
end end
@ -44,8 +45,8 @@ def cast_and_validate(data) do
def validate_accept_reject_rights(cng) do def validate_accept_reject_rights(cng) do
with object_id when is_binary(object_id) <- get_field(cng, :object), with object_id when is_binary(object_id) <- get_field(cng, :object),
%Activity{data: %{"object" => followed_actor}} <- Activity.get_by_ap_id(object_id), activity <- Activity.get_by_ap_id(object_id),
true <- followed_actor == get_field(cng, :actor) do true <- validate_actor(activity, get_field(cng, :actor)) do
cng cng
else else
_e -> _e ->
@ -53,4 +54,13 @@ def validate_accept_reject_rights(cng) do
|> add_error(:actor, "can't accept or reject the given activity") |> add_error(:actor, "can't accept or reject the given activity")
end end
end end
defp validate_actor(%Activity{data: %{"type" => "Follow", "object" => followed_actor}}, actor) do
followed_actor == actor
end
defp validate_actor(%Activity{data: %{"type" => "Join", "object" => joined_event}}, actor) do
%Object{data: %{"actor" => event_author}} = Object.get_cached_by_ap_id(joined_event)
event_author == actor
end
end end

View file

@ -72,6 +72,8 @@ defmacro event_object_fields do
field(:startTime, ObjectValidators.DateTime) field(:startTime, ObjectValidators.DateTime)
field(:endTime, ObjectValidators.DateTime) field(:endTime, ObjectValidators.DateTime)
field(:joinMode, :string, default: "free")
embeds_one(:location, PlaceValidator) embeds_one(:location, PlaceValidator)
end end
end end

View file

@ -64,6 +64,7 @@ def changeset(struct, data) do
defp validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Event"]) |> validate_inclusion(:type, ["Event"])
|> validate_inclusion(:joinMode, ~w[free restricted invite])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])
|> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo]) |> CommonValidations.validate_fields_match([:actor, :attributedTo])

View file

@ -0,0 +1,87 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.JoinValidator do
use Ecto.Schema
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.Utils
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
quote do
unquote do
import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields
message_fields()
activity_fields()
end
end
field(:state, :string, default: "pending")
field(:participationMessage, :string)
end
def cast_data(data) do
data =
data
|> fix()
%__MODULE__{}
|> changeset(data)
end
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
end
defp fix(data) do
data =
data
|> CommonFixes.fix_actor()
|> CommonFixes.fix_activity_addressing()
with %Object{} = object <- Object.normalize(data["object"]) do
data
|> CommonFixes.fix_activity_context(object)
|> CommonFixes.fix_object_action_recipients(object)
else
_ -> data
end
end
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Join"])
|> validate_inclusion(:state, ~w{pending reject accept})
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc])
|> validate_actor_presence()
|> validate_object_presence(allowed_types: ["Event"])
|> validate_existing_join()
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
defp validate_existing_join(%{changes: %{actor: actor, object: object}} = cng) do
if Utils.get_existing_join(actor, %{data: %{"id" => object}}) do
cng
|> add_error(:actor, "already joined this event")
|> add_error(:object, "already joined by this actor")
else
cng
end
end
defp validate_existing_join(cng), do: cng
end

View file

@ -46,19 +46,14 @@ def handle(
data: %{ data: %{
"actor" => actor, "actor" => actor,
"type" => "Accept", "type" => "Accept",
"object" => follow_activity_id "object" => activity_id
} }
} = object, } = object,
meta meta
) do ) do
with %Activity{actor: follower_id} = follow_activity <- with %Activity{} = activity <-
Activity.get_by_ap_id(follow_activity_id), Activity.get_by_ap_id(activity_id) do
%User{} = followed <- User.get_cached_by_ap_id(actor), handle_accepted(activity, actor)
%User{} = follower <- User.get_cached_by_ap_id(follower_id),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _follower, followed} <-
FollowingRelationship.update(follower, followed, :follow_accept) do
Notification.update_notification_type(followed, follow_activity)
end end
{:ok, object, meta} {:ok, object, meta}
@ -74,21 +69,63 @@ def handle(
data: %{ data: %{
"actor" => actor, "actor" => actor,
"type" => "Reject", "type" => "Reject",
"object" => follow_activity_id "object" => activity_id
} }
} = object, } = object,
meta meta
) do ) do
with %Activity{actor: follower_id} = follow_activity <- with %Activity{actor: follower_id} = activity <-
Activity.get_by_ap_id(follow_activity_id), Activity.get_by_ap_id(activity_id) do
%User{} = followed <- User.get_cached_by_ap_id(actor), handle_rejected(activity, actor)
end
{:ok, object, meta}
end
defp handle_accepted(
%Activity{actor: follower_id, data: %{"type" => "Follow"}} = follow_activity,
actor
) do
with %User{} = followed <- User.get_cached_by_ap_id(actor),
%User{} = follower <- User.get_cached_by_ap_id(follower_id),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _follower, followed} <-
FollowingRelationship.update(follower, followed, :follow_accept) do
Notification.update_notification_type(followed, follow_activity)
end
end
defp handle_accepted(
%Activity{data: %{"type" => "Join", "object" => event_id}} = join_activity,
_actor
) do
with joined_event <- Object.get_by_ap_id(event_id),
{:o, join_activity} <- Utils.update_follow_state(join_activity, "accept") do
Utils.add_participation_to_object(join_activity, joined_event)
# Notification.update_notification_type(followed, follow_activity)
end
end
defp handle_rejected(
%Activity{actor: follower_id, data: %{"type" => "Follow"}} = follow_activity,
actor
) do
with %User{} = followed <- User.get_cached_by_ap_id(actor),
%User{} = follower <- User.get_cached_by_ap_id(follower_id), %User{} = follower <- User.get_cached_by_ap_id(follower_id),
{:ok, _follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject") do {:ok, _follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject") do
FollowingRelationship.update(follower, followed, :follow_reject) FollowingRelationship.update(follower, followed, :follow_reject)
Notification.dismiss(follow_activity) Notification.dismiss(follow_activity)
end end
end
{:ok, object, meta} defp handle_rejected(
%Activity{data: %{"type" => "Join", "object" => event_id}} = join_activity,
_actor
) do
with joined_event <- Object.get_by_ap_id(event_id),
{:o, join_activity} <- Utils.update_join_state(join_activity, "reject") do
Utils.remove_participation_from_object(join_activity, joined_event)
end
end end
# Tasks this handle # Tasks this handle
@ -384,6 +421,20 @@ def handle(%{data: %{"type" => "Remove"} = data} = object, meta) do
end end
end end
# Tasks this handles:
# accepts join if event is local
@impl true
def handle(%{data: %{"type" => "Join"}} = object, meta) do
joined_event = Object.get_by_ap_id(object.data["object"])
if Object.local?(joined_event) and joined_event.data["joinMode"] == "free" do
{:ok, accept_data, _} = Builder.accept(joined_event, object)
{:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true)
end
{:ok, object, meta}
end
# Nothing to do # Nothing to do
@impl true @impl true
def handle(object, meta) do def handle(object, meta) do

View file

@ -432,6 +432,29 @@ defp fetch_likes(object) do
end end
end end
def add_participation_to_object(%Activity{data: %{"actor" => actor}}, object) do
[actor | fetch_participations(object)]
|> Enum.uniq()
|> update_participations_in_object(object)
end
def remove_participation_from_object(%Activity{data: %{"actor" => actor}}, object) do
List.delete(fetch_participations(object), actor)
|> update_participations_in_object(object)
end
defp update_participations_in_object(participations, object) do
update_element_in_object("participation", participations, object)
end
defp fetch_participations(object) do
if is_list(object.data["participations"]) do
object.data["participations"]
else
[]
end
end
#### Follow-related helpers #### Follow-related helpers
@doc """ @doc """
@ -887,4 +910,26 @@ def get_existing_votes(actor, %{data: %{"id" => id}}) do
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|> Repo.all() |> Repo.all()
end end
### Join-related helpers
def get_existing_join(actor, %{data: %{"id" => id}}) do
actor
|> Activity.Queries.by_actor()
|> Activity.Queries.by_object_id(id)
|> Activity.Queries.by_type("Join")
|> limit(1)
|> Repo.one()
end
def update_join_state(
%Activity{} = activity,
state
) do
new_data = Map.put(activity.data, "state", state)
changeset = Changeset.change(activity, data: new_data)
with {:ok, activity} <- Repo.update(changeset) do
{:ok, activity}
end
end
end end

View file

@ -0,0 +1,100 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaEventOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Status
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def create_operation do
%Operation{
tags: ["Event actions"],
summary: "Publish new status",
security: [%{"oAuth" => ["write"]}],
description: "Create a new event",
operationId: "PleromaAPI.EventController.create",
requestBody: request_body("Parameters", create_request(), required: true),
responses: %{
200 => event_response(),
422 => Operation.response("Bad Request", "application/json", ApiError)
}
}
end
def participate_operation do
%Operation{
tags: ["Event actions"],
summary: "Participate",
security: [%{"oAuth" => ["write"]}],
description: "Participate in an event",
operationId: "PleromaAPI.EventController.participate",
parameters: [id_param()],
responses: %{
200 => event_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
defp create_request do
%Schema{
title: "EventCreateRequest",
type: :object,
properties: %{
name: %Schema{
type: :string,
description: "Name of the event."
},
content: %Schema{
type: :string,
description: "Text description of the event."
},
start_time: %Schema{
type: :string,
format: :"date-time",
description: "Start time."
},
end_time: %Schema{
type: :string,
format: :"date-time",
description: "End time."
},
join_mode: %Schema{
type: :string,
enum: ["free", "restricted"]
}
},
example: %{
"name" => "Example event",
"content" => "No information for now.",
"start_time" => "21-02-2022 22:00:00",
"end_time" => "21-02-2022 23:00:00"
}
}
end
defp event_response do
Operation.response(
"Status",
"application/json",
Status
)
end
defp id_param do
Operation.parameter(:id, :path, FlakeID, "Event ID",
example: "9umDrYheeY451cQnEe",
required: true
)
end
end

View file

@ -304,6 +304,43 @@ def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
end end
end end
def join(%User{} = user, id) do
case join_helper(user, id) do
{:ok, _} = res ->
res
{:error, :not_found} = res ->
res
{:error, e} ->
Logger.error("Could not join #{id}. Error: #{inspect(e, pretty: true)}")
{:error, dgettext("errors", "Could not join")}
end
end
def join_helper(user, id) do
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
{_, {:ok, join_object, meta}} <- {:build_object, Builder.join(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(join_object, Keyword.put(meta, :local, true))} do
{:ok, activity}
else
{:find_object, _} ->
{:error, :not_found}
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
if {:object, {"already joined by this actor", []}} in changeset.errors do
{:ok, :already_joined}
else
{:error, e}
end
e ->
{:error, e}
end
end
defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}), defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
do: {:error, dgettext("errors", "Poll's author can't vote")} do: {:error, dgettext("errors", "Poll's author can't vote")}
@ -598,4 +635,10 @@ def get_user(ap_id, fake_record_fallback \\ true) do
nil nil
end end
end end
def event(user, data) do
with {:ok, draft} <- ActivityDraft.event(user, data) do
ActivityPub.create(draft.changes)
end
end
end end

View file

@ -88,6 +88,32 @@ defp listen_object(draft) do
%__MODULE__{draft | object: object} %__MODULE__{draft | object: object}
end end
def event(user, params) do
user
|> new(params)
|> visibility()
|> to_and_cc()
|> context()
|> event_object()
|> with_valid(&changes/1)
|> validate()
end
defp event_object(draft) do
object =
draft.params
|> Map.take([:title, :content])
|> Map.put("type", "Event")
|> Map.put("to", draft.to)
|> Map.put("cc", draft.cc)
|> Map.put("actor", draft.user.ap_id)
|> Map.put("startTime", draft.params[:start_time])
|> Map.put("endTime", draft.params[:end_time])
|> Map.put("joinMode", draft.params[:join_mode])
%__MODULE__{draft | object: object}
end
defp put_params(draft, params) do defp put_params(draft, params) do
params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id]) params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
%__MODULE__{draft | params: params} %__MODULE__{draft | params: params}

View file

@ -562,6 +562,8 @@ def build_event(%{"type" => "Event"} = data) do
%{ %{
start_time: data["startTime"], start_time: data["startTime"],
end_time: data["endTime"], end_time: data["endTime"],
join_mode: data["joinMode"],
participants_count: data["participant_count"]
} }
end end

View file

@ -0,0 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EventController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [try_render: 3]
alias Pleroma.Activity
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.CommonAPI
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
%{scopes: ["write"]}
when action in [:create, :participate]
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEventOperation
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, activity} <- CommonAPI.event(user, params) do
conn
|> put_view(StatusView)
|> try_render("show.json",
activity: activity,
for: user,
as: :activity
)
else
{:error, {:reject, message}} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
{:error, message} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
end
end
def participations(conn, %{"id" => activity_id}) do
end
def participation_requests(conn, %{"id" => activity_id}) do
%Activity{object: %Object{data: %{"id" => ap_id}}} = activity <-
Activity.get_by_id_with_object(activity_id)
params =
params
|> Map.put(:type, "Join")
|> Map.put(:object, ap_id)
|> Map.put(:state, "pending")
activities =
recipients
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
end
def participate(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
with {:ok, _} <- CommonAPI.join(user, activity_id),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
conn
|> put_view(StatusView)
|> try_render("show.json", activity: activity, for: user, as: :activity)
end
end
end

View file

@ -0,0 +1,24 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EventView do
use Pleroma.Web, :view
alias Pleroma.Web.MastodonAPI
def render(
"participation_requests.json",
%{participation_requests: participation_requests} = opts
) do
render_many(
participation_requests,
__MODULE__,
"participation_request.json",
Map.delete(opts, :participation_requests)
)
end
def render("participation_request.json", %{participation_request: participation_request} = opts) do
%{}
end
end

View file

@ -441,6 +441,9 @@ defmodule Pleroma.Web.Router do
get("/backups", BackupController, :index) get("/backups", BackupController, :index)
post("/backups", BackupController, :create) post("/backups", BackupController, :create)
post("/events", EventController, :create)
post("/events/:id/participate", EventController, :participate)
end end
scope [] do scope [] do

View file

@ -36,7 +36,16 @@
"@id": "as:alsoKnownAs", "@id": "as:alsoKnownAs",
"@type": "@id" "@type": "@id"
}, },
"vcard": "http://www.w3.org/2006/vcard/ns#" "vcard": "http://www.w3.org/2006/vcard/ns#",
"mz": "https://joinmobilizon.org/ns#",
"joinMode": {
"@id": "mz:joinMode",
"@type": "mz:joinModeType"
},
"joinModeType": {
"@id": "mz:joinModeType",
"@type": "rdfs:Class"
}
} }
] ]
} }