Merge branch 'events' into 'develop'

Mobilizon-compatible events

See merge request pleroma/pleroma!3955
This commit is contained in:
mkljczk 2025-03-23 14:05:24 +00:00
commit fd1a614520
77 changed files with 3884 additions and 94 deletions

1
changelog.d/events.add Normal file
View file

@ -0,0 +1 @@
Mobilizon-compatible events

View file

@ -499,7 +499,7 @@ config :pleroma, :shout,
enabled: true,
limit: 5_000
config :phoenix, :format_encoders, json: Jason, "activity+json": Jason
config :phoenix, :format_encoders, json: Jason, "activity+json": Jason, ics: ICalendar
config :phoenix, :json_library, Jason
@ -725,6 +725,7 @@ config :pleroma, :rate_limit,
relation_id_action: {60_000, 2},
statuses_actions: {10_000, 15},
status_id_action: {60_000, 3},
events_actions: {10_000, 15},
password_reset: {1_800_000, 5},
account_confirmation_resend: {8_640_000, 5},
ap_routes: {60_000, 15}
@ -957,6 +958,22 @@ config :pleroma, Pleroma.Search.QdrantSearch,
vectors: %{size: 384, distance: "Cosine"}
}
config :geospatial, Geospatial.Service, service: Geospatial.Providers.Nominatim
config :geospatial, Geospatial.Providers.GoogleMaps,
api_key: nil,
fetch_place_details: true
config :geospatial, Geospatial.Providers.Nominatim,
endpoint: "https://nominatim.openstreetmap.org",
api_key: nil
config :geospatial, Geospatial.Providers.Pelias,
endpoint: "https://api.geocode.earth",
api_key: nil
config :geospatial, Geospatial.HTTP, user_agent: &Pleroma.Application.user_agent/0
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

View file

@ -2613,6 +2613,12 @@ config :pleroma, :config_description, [
"For fav / unfav or reblog / unreblog actions on the same status by the same user",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :events_actions,
type: [:tuple, {:list, :tuple}],
description: "For create / update / join / leave actions on any statuses",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :authentication,
type: [:tuple, {:list, :tuple}],
@ -3566,5 +3572,82 @@ config :pleroma, :config_description, [
suggestions: ["YOUR_API_KEY"]
}
]
},
%{
group: :geospatial,
key: Geospatial.Service,
type: :group,
description: "Geospatial service providers",
children: [
%{
key: :service,
type: :module,
label: "Geospatial service provider",
suggestions: [
Geospatial.Providers.GoogleMaps,
Geospatial.Providers.Nominatim,
Geospatial.Providers.Pelias
]
}
]
},
%{
group: :geospatial,
key: Geospatial.Providers.Nominatim,
type: :group,
description: "Nominatim provider configuration",
children: [
%{
key: :endpoint,
type: :string,
description: "Nominatim endpoint",
suggestions: ["https://nominatim.openstreetmap.org"]
},
%{
key: :api_key,
type: :string,
description: "Nominatim API key",
suggestions: [nil]
}
]
},
%{
group: :geospatial,
key: Geospatial.Providers.GoogleMaps,
type: :group,
description: "Google Maps provider configuration",
children: [
%{
key: :api_key,
type: :string,
description: "Google Maps API key",
suggestions: [nil]
},
%{
key: :fetch_place_details,
type: :boolean,
description: "Fetch place details"
}
]
},
%{
group: :geospatial,
key: Geospatial.Providers.Pelias,
type: :group,
description: "Pelias provider configuration",
children: [
%{
key: :endpoint,
type: :string,
description: "Pelias endpoint",
suggestions: ["https://api.geocode.earth"]
},
%{
key: :api_key,
type: :string,
description: "Pelias API key",
suggestions: [nil]
}
]
}
]

View file

@ -62,6 +62,8 @@ config :pleroma, :password, iterations: 1
config :tesla, adapter: Tesla.Mock
config :tesla, Geospatial.HTTP, adapter: Tesla.Mock
config :pleroma, :rich_media,
enabled: false,
ignore_hosts: [],

View file

@ -525,6 +525,7 @@ Supported rate limiters:
* `:relation_id_action` - Following/Unfollowing for a specific user.
* `:statuses_actions` - Status actions such as: (un)repeating, (un)favouriting, creating, deleting.
* `:status_id_action` - (un)Repeating/(un)Favouriting a particular status.
* `:events_actions` - Events actions such as: creating, joining, leaving.
* `:authentication` - Authentication actions, i.e getting an OAuth token.
* `:password_reset` - Requesting password reset emails.
* `:account_confirmation_resend` - Requesting resending account confirmation emails.

View file

@ -43,11 +43,39 @@ Has these additional fields under the `pleroma` object:
- `non_anonymous`: true if the source post specifies the poll results are not anonymous. Currently only implemented by Smithereen.
- `bookmark_folder`: the ID of the folder bookmark is stored within (if any).
- `list_id`: the ID of the list the post is addressed to (if any, only returned to author).
- `event`: event information if the post is an event, `null` otherwise.
The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes:
- `content_type`: The content type of the status source.
### Event
Event object includes following fields:
- `name`: event name.
- `start_time`: datetime, if specified, the time when the event starts, `null` otherwise.
- `end_time`: datetime, if specified, the time when the event finishes, `null` otherwise.
- `join_mode`: who can join the event. Possible values, if specified: `free`, `restricted` and `invite`. `null` otherwise.
- `participants_count`: the number of users who joined the event.
- `location`: event location, if specified, `null` otherwise.
- `join_state`: whether the user joined the event. Possible values: `pending`, `reject`, `accept`. `null`, if no `Join` exists.
- `participation_request_count`: the number of users who requested to join the event.
### Event location
Event location object includes following fields:
- `name`: place name.
- `url`: location url address or `null`.
- `longitude`: X-coordinate of the place or `null`.
- `latitude`: Y-coordinate of the place or `null`.
- `street`: place street or `null`.
- `postal_code`: place postal code or `null`.
- `locality`: place city or `null`.
- `region`: place region or `null`.
- `country`: place country or `null`.
## Scheduled statuses
Has these additional fields in `params`:
@ -175,6 +203,32 @@ The `type` value is `pleroma:emoji_reaction`. Has these fields:
- `account`: The account of the user who reacted
- `status`: The status that was reacted on
### EventReminder Notification
The `type` value is `pleroma:event_reminder`. Has these fields:
- `status`: The event status
### EventUpdate Notification
The `type` value is `pleroma:event_update`. Has these fields:
- `status`: The event status
### ParticipationAccepted Notification
The `type` value is `pleroma:participation_accepted`. Has these fields:
- `status`: The event status
- `participation_message`: Participation request message
### ParticipationRequest Notification
The `type` value is `pleroma:participation_request`. Has these fields:
- `status`: The event status
- `participation_message`: Participation request message
### ChatMention Notification (not default)
This notification has to be requested explicitly.

View file

@ -333,6 +333,117 @@ Deprecated. `notify` parameter in `POST /api/v1/accounts/:id/follow` should be u
* `id`: folder id
* Response: JSON. Returns a single bookmark folder.
## `/api/v1/pleroma/events`
### Creates an event
* Method `POST`
* Authentication: required
* Params:
* `name`: name of the event
* `status`: optional, description of the event
* `banner_id`: optional, event banner attachment ID
* `start_time`: start time of the event
* `end_time`: optional, end time of the event
* `join_mode`: optional, event join mode, either `free` or `restricted`, defaults to `free`
* `location_id`: optional, location ID from the location provider used by server
* Response: JSON. Returns a Mastodon Status entity.
## `/api/v1/pleroma/events/joined_events`
### Gets user's joined events
* Method `GET`
* Authentication: required
* Response: JSON. Returns a list of Mastodon Status entities.
## `/api/v1/pleroma/events/:id`
### Edits an event
* Method `POST`
* Authentication: required
* Params:
* `id`: ID of the status
* `name`: optional, name of the event
* `status`: optional, description of the event
* `banner_id`: optional, event banner attachment ID
* `start_time`: optional, start time of the event
* `end_time`: optional, end time of the event
* `location_id`: optional, location ID from the location provider used by server
* Response: JSON. Returns a Mastodon Status entity.
## `/api/v1/pleroma/events/:id/participations`
### Gets event participants
* Method `GET`
* Authentication: required
* Params:
* `id`: ID of the status
* Response: JSON. Returns a list of Mastodon Account entities.
## `/api/v1/pleroma/events/:id/participation_requests`
### Gets event participation requests
* Method `GET`
* Authentication: required
* Params:
* `id`: ID of the status
* Response: JSON. Returns a list of `{"account": "[Mastodon Account entity]", "participation_message": "[Participation request message]"}` entities.
## `/api/v1/pleroma/events/:id/participation_requests/:participant_id/authorize`
### Accepts user to the event
* Method `POST`
* Authentication: required
* Params:
* `id`: ID of the status
* `participant_id`: ID of the account
* Response: JSON. Returns a Mastodon Status entity.
## `/api/v1/pleroma/events/:id/participation_requests/:participant_id/reject`
### Rejects user from the event
* Method `POST`
* Authentication: required
* Params:
* `id`: ID of the status
* `participant_id`: ID of the account
* Response: JSON. Returns a Mastodon Status entity.
## `/api/v1/pleroma/events/:id/join`
### Joins the event
* Method `POST`
* Authentication: required
* Params:
* `id`: ID of the status
* Response: JSON. Returns a Mastodon Status entity.
## `/api/v1/pleroma/events/:id/leave`
### Leaves the event
* Method `POST`
* Authentication: required
* Params:
* `id`: ID of the status
* Response: JSON. Returns a Mastodon Status entity.
## `/api/v1/pleroma/events/:id/ics`
### Event ICS file
* Method `GET`
* Authentication: not required
* Params:
* `id`: ID of the status
* Response: ICS. Returns calendar file for the event.
## `/api/v1/pleroma/search/location`
### Searches for locations
* Method `GET`
* Authentication: required
* Params:
* `q`: Search query
* Response: JSON. Returns a list of found locations.
## `/api/v1/pleroma/mascot`
### Gets user mascot image
* Method `GET`

View file

@ -297,12 +297,18 @@ defmodule Pleroma.Activity do
def normalize(_), do: nil
def delete_all_by_object_ap_id(id) when is_binary(id) do
id
|> Queries.by_object_id()
|> Queries.exclude_type("Delete")
|> select([u], u)
|> Repo.delete_all(timeout: :infinity)
|> elem(1)
activities =
id
|> Queries.by_object_id()
|> Queries.exclude_type("Delete")
|> select([u], u)
|> Repo.delete_all(timeout: :infinity)
|> elem(1)
activities
|> Enum.each(fn %{data: %{"id" => ap_id}} -> delete_all_by_object_ap_id(ap_id) end)
activities
|> Enum.find(fn
%{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
%{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id

View file

@ -111,7 +111,8 @@ defmodule Pleroma.Application do
Pleroma.JobQueueMonitor,
{Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]},
{Oban, Config.get(Oban)},
Pleroma.Web.Endpoint
Pleroma.Web.Endpoint,
TzWorld.Backend.DetsWithIndexCache
] ++
task_children() ++
streamer_registry() ++

View file

@ -271,7 +271,7 @@ defmodule Pleroma.ConfigDB do
":#{entity}"
end
def to_json_types(entity) when is_atom(entity), do: inspect(entity)
def to_json_types(entity) when is_atom(entity) or is_function(entity), do: inspect(entity)
@spec to_elixir_types(boolean() | String.t() | map() | list()) :: term()
def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do

View file

@ -21,7 +21,11 @@ defmodule Pleroma.Constants do
"pleroma_internal",
"generator",
"rules",
"language"
"language",
"participations",
"participation_count",
"participation_request_count",
"location_id"
]
)
@ -42,7 +46,13 @@ defmodule Pleroma.Constants do
"sensitive",
"attachment",
"generator",
"language"
"language",
"startTime",
"endTime",
"location",
"location_id",
"location_provider",
"name"
]
)
@ -104,7 +114,9 @@ defmodule Pleroma.Constants do
"Undo",
"Flag",
"EmojiReact",
"Listen"
"Listen",
"Join",
"Leave"
]
)

View file

@ -74,6 +74,10 @@ defmodule Pleroma.Notification do
reblog
poll
status
pleroma:participation_accepted
pleroma:participation_request
pleroma:event_reminder
pleroma:event_update
}
def changeset(%Notification{} = notification, attrs) do
@ -367,23 +371,37 @@ defmodule Pleroma.Notification do
end
def create_notifications(%Activity{data: %{"type" => type}} = activity)
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
when type in [
"Follow",
"Like",
"Announce",
"Move",
"EmojiReact",
"Flag",
"Update",
"Accept",
"Join"
] do
do_create_notifications(activity)
end
def create_notifications(_), do: {:ok, []}
defp do_create_notifications(%Activity{} = activity) do
enabled_participants = get_notified_participants_from_activity(activity)
enabled_receivers = get_notified_from_activity(activity)
enabled_subscribers = get_notified_subscribers_from_activity(activity)
notifications =
(Enum.map(enabled_receivers, fn user ->
(Enum.map(enabled_receivers -- enabled_participants, fn user ->
create_notification(activity, user)
end) ++
Enum.map(enabled_subscribers -- enabled_receivers, fn user ->
create_notification(activity, user, type: "status")
end) ++
Enum.map(enabled_participants, fn user ->
create_notification(activity, user, type: "pleroma:event_update")
end))
|> Enum.reject(&is_nil/1)
@ -425,6 +443,12 @@ defmodule Pleroma.Notification do
"Update" ->
"update"
"Accept" ->
"pleroma:participation_accepted"
"Join" ->
"pleroma:participation_request"
t ->
raise "No notification type for activity type #{t}"
end
@ -483,6 +507,28 @@ defmodule Pleroma.Notification do
end
end
def create_event_notifications(%Activity{} = activity) do
with %Object{data: %{"type" => "Event", "actor" => actor} = data} <-
Object.normalize(activity) do
participations =
case data do
%{"participations" => participations} when is_list(participations) -> participations
_ -> []
end
notifications =
Enum.reduce([actor | participations], [], fn ap_id, acc ->
with %User{local: true} = user <- User.get_by_ap_id(ap_id) do
[create_notification(activity, user, type: "pleroma:event_reminder") | acc]
else
_ -> acc
end
end)
{:ok, notifications}
end
end
@doc """
Returns a tuple with 2 elements:
{notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
@ -501,7 +547,9 @@ defmodule Pleroma.Notification do
"Move",
"EmojiReact",
"Flag",
"Update"
"Update",
"Accept",
"Join"
] do
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
@ -537,6 +585,24 @@ defmodule Pleroma.Notification do
def get_notified_subscribers_from_activity(_, _), do: []
def get_notified_participants_from_activity(activity, local_only \\ true)
def get_notified_participants_from_activity(
%Activity{data: %{"type" => "Update"}} = activity,
local_only
) do
notification_enabled_ap_ids =
[]
|> Utils.maybe_notify_participants(activity)
potential_receivers =
User.get_users_from_set(notification_enabled_ap_ids, local_only: local_only)
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
end
def get_notified_participants_from_activity(_, _), do: []
# For some activities, only notify the author of the object
def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
when type in ~w{Like Announce EmojiReact} do
@ -549,6 +615,35 @@ defmodule Pleroma.Notification do
end
end
def get_potential_receiver_ap_ids(%{data: %{"type" => "Accept", "object" => join_id}}) do
case Activity.get_by_ap_id_with_object(join_id) do
%Activity{
data: %{"type" => "Join"},
object: %Object{data: %{"type" => "Event", "joinMode" => "free"}}
} ->
[]
%Activity{data: %{"type" => "Join", "actor" => actor_id}} ->
[actor_id]
_ ->
[]
end
end
def get_potential_receiver_ap_ids(%{data: %{"type" => "Join", "object" => object_id}}) do
case Object.get_by_ap_id(object_id) do
%Object{data: %{"type" => "Event", "joinMode" => "free"}} ->
[]
%Object{data: %{"type" => "Event", "actor" => actor_id}} ->
[actor_id]
_ ->
[]
end
end
def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
[object_id]
end

View file

@ -20,11 +20,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Repo
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker
alias Pleroma.Workers.EventReminderWorker
alias Pleroma.Workers.PollWorker
import Ecto.Query
@ -321,6 +324,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
_ <- notify_and_stream(activity),
:ok <- maybe_schedule_poll_notifications(activity),
:ok <- maybe_handle_group_posts(activity),
:ok <- maybe_schedule_event_notifications(activity),
:ok <- maybe_join_own_event(actor, activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@ -340,6 +345,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
:ok
end
defp maybe_schedule_event_notifications(activity) do
EventReminderWorker.schedule_event_reminder(activity)
:ok
end
defp maybe_join_own_event(actor, %{object: %{data: %{"type" => "Event"}} = object}) do
{:ok, join_object, meta} = Builder.join(actor, object)
{:ok, _, _} = Pipeline.common_pipeline(join_object, Keyword.put(meta, :local, true))
:ok
end
defp maybe_join_own_event(_, _), do: :ok
@spec listen(map()) :: {:ok, Activity.t()} | {:error, any()}
def listen(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{}
@ -482,9 +502,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> where(
[activity],
fragment(
"?->>'type' = ? and ?->>'context' = ?",
"?->>'type' = 'Create' and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
)
@ -999,7 +1018,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_media(query, %{only_media: true}) do
from(
[activity, object] in query,
where: fragment("(?)->>'type' = ?", activity.data, "Create"),
where: fragment("(?)->>'type' = 'Create'", activity.data),
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end
@ -1282,6 +1301,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_unauthenticated(query, _), do: query
defp restrict_object(query, %{object: object}) do
from(activity in query, where: fragment("?->>'object' = ?", activity.data, ^object))
end
defp restrict_object(query, _), do: query
defp restrict_join_state(query, %{state: state}) when is_binary(state) do
from(
[activity] in query,
where: fragment("(?)->>'state' = ?", activity.data, ^state)
)
end
defp restrict_join_state(query, _), do: query
defp restrict_quote_url(query, %{quote_url: quote_url}) do
from([_activity, object] in query,
where: fragment("(?)->'quoteUrl' = ?", object.data, ^quote_url)
@ -1304,7 +1338,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do
from([activity, object: o] in query,
where: fragment("not(?->>'type' = ?)", o.data, "Answer")
where: fragment("not(?->>'type' = 'Answer')", o.data)
)
else
query
@ -1316,7 +1350,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp exclude_chat_messages(query, _) do
if has_named_binding?(query, :object) do
from([activity, object: o] in query,
where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage")
where: fragment("not(?->>'type' = 'ChatMessage')", o.data)
)
else
query
@ -1462,6 +1496,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_announce_object_actor(opts)
|> restrict_filtered(opts)
|> restrict_rule(opts)
|> restrict_object(opts)
|> restrict_quote_url(opts)
|> maybe_restrict_deactivated_users(opts)
|> exclude_poll_votes(opts)
@ -1896,4 +1931,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp maybe_restrict_deactivated_users(activity, _opts),
do: Activity.restrict_deactivated_users(activity)
def fetch_joined_events(user, params \\ %{}, pagination \\ :keyset) do
user.ap_id
|> Activity.Queries.by_actor()
|> Activity.Queries.by_type("Join")
|> Activity.with_joined_object()
|> Object.with_joined_activity()
|> select([join, object, activity], %{activity | object: object, pagination_id: join.id})
|> order_by([join, _, _], desc_nulls_last: join.id)
|> restrict_join_state(params)
|> Pagination.fetch_paginated(
Map.merge(params, %{skip_order: true}),
pagination
)
end
end

View file

@ -21,10 +21,10 @@ defmodule Pleroma.Web.ActivityPub.Builder do
require Pleroma.Constants
def accept_or_reject(actor, activity, type) do
def accept_or_reject(%User{ap_id: ap_id}, activity, type) do
data = %{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
"actor" => ap_id,
"type" => type,
"object" => activity.data["id"],
"to" => [activity.actor]
@ -33,14 +33,26 @@ defmodule Pleroma.Web.ActivityPub.Builder do
{:ok, data, []}
end
@spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()}
def reject(actor, rejected_activity) do
accept_or_reject(actor, rejected_activity, "Reject")
def accept_or_reject(%Object{data: %{"actor" => actor}}, activity, type) do
data = %{
"id" => Utils.generate_activity_id(),
"actor" => actor,
"type" => type,
"object" => activity.data["id"],
"to" => [activity.actor]
}
{:ok, data, []}
end
@spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()}
def accept(actor, accepted_activity) do
accept_or_reject(actor, accepted_activity, "Accept")
@spec reject(User.t() | Object.t(), Activity.t()) :: {:ok, map(), keyword()}
def reject(object, rejected_activity) do
accept_or_reject(object, rejected_activity, "Reject")
end
@spec accept(User.t() | Object.t(), Activity.t()) :: {:ok, map(), keyword()}
def accept(object, accepted_activity) do
accept_or_reject(object, accepted_activity, "Accept")
end
@spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
@ -307,13 +319,13 @@ defmodule Pleroma.Web.ActivityPub.Builder do
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
def update(actor, object) do
{to, cc} =
{to, cc, bcc} =
if object["type"] in Pleroma.Constants.actor_types() do
# User updates, always public
{[Pleroma.Constants.as_public(), actor.follower_address], []}
{[Pleroma.Constants.as_public(), actor.follower_address], [], []}
else
# Status updates, follow the recipients in the object
{object["to"] || [], object["cc"] || []}
{object["to"] || [], object["cc"] || [], object["participations"] || []}
end
{:ok,
@ -323,7 +335,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
"actor" => actor.ap_id,
"object" => object,
"to" => to,
"cc" => cc
"cc" => cc,
"bcc" => bcc
}, []}
end
@ -431,4 +444,38 @@ defmodule Pleroma.Web.ActivityPub.Builder do
defp pinned_url(nickname) when is_binary(nickname) do
Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
end
def join(actor, object, participation_message \\ nil) do
with {:ok, data, meta} <- object_action(actor, object) do
data =
data
|> Map.put("type", "Join")
|> Map.put("participationMessage", participation_message)
{:ok, data, meta}
end
end
@spec event(ActivityDraft.t()) :: {:ok, map(), keyword()}
def event(%ActivityDraft{} = draft) do
data = %{
"type" => "Event",
"to" => draft.to,
"cc" => draft.cc,
"name" => draft.params[:name],
"content" => draft.content_html,
"context" => draft.context,
"attachment" => draft.attachments,
"actor" => draft.user.ap_id,
"tag" => Keyword.values(draft.tags) |> Enum.uniq(),
"joinMode" => draft.params[:join_mode] || "free",
"location" => draft.location,
"location_id" => draft.location_id,
"location_provider" => draft.location_provider,
"startTime" => draft.start_time,
"endTime" => draft.end_time
}
{:ok, data, []}
end
end

View file

@ -33,6 +33,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EventValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.JoinValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LeaveValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
@ -201,7 +203,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
ChatMessage Answer] do
ChatMessage Answer Join Leave] do
validator =
case type do
"Accept" -> AcceptRejectValidator
@ -213,6 +215,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
"Announce" -> AnnounceValidator
"ChatMessage" -> ChatMessageValidator
"Answer" -> AnswerValidator
"Join" -> JoinValidator
"Leave" -> LeaveValidator
end
cast_func =

View file

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

View file

@ -5,8 +5,10 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.PlaceValidator
alias Pleroma.Web.ActivityPub.Transmogrifier
import Ecto.Changeset
@ -24,6 +26,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
status_object_fields()
end
end
field(:startTime, ObjectValidators.DateTime)
field(:endTime, ObjectValidators.DateTime)
field(:joinMode, :string, default: "free")
embeds_one(:location, PlaceValidator)
field(:participation_count, :integer, default: 0)
field(:participations, {:array, ObjectValidators.ObjectID}, default: [])
field(:participation_request_count, :integer, default: 0)
end
def cast_and_apply(data) do
@ -58,14 +71,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
data = fix(data)
struct
|> cast(data, __schema__(:fields) -- [:attachment, :tag])
|> cast(data, __schema__(:fields) -- [:attachment, :tag, :location])
|> cast_embed(:attachment)
|> cast_embed(:tag)
|> cast_embed(:location)
end
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Event"])
|> validate_inclusion(:joinMode, ~w[free restricted invite external])
|> validate_required([:id, :actor, :attributedTo, :type, :context])
|> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo])

View file

@ -0,0 +1,61 @@
# 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.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
%__MODULE__{}
|> cast(data, __schema__(:fields))
end
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Join"])
|> validate_inclusion(:state, ~w{pending reject accept})
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> 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, 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

@ -0,0 +1,56 @@
# 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.LeaveValidator do
use Ecto.Schema
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
end
def cast_data(data) do
%__MODULE__{}
|> cast(data, __schema__(:fields))
end
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Leave"])
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> 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, object) do
cng
|> add_error(:actor, "not joined this event")
|> add_error(:object, "not joined by this actor")
else
cng
end
end
defp validate_existing_join(cng), do: cng
end

View file

@ -0,0 +1,71 @@
# 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.PlaceValidator do
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:type, :string)
field(:name, :string)
field(:longitude, :float)
field(:latitude, :float)
field(:accuracy, :float)
field(:altitude, :float)
field(:radius, :float)
field(:units, :string)
embeds_one :address, Address do
field(:type, :string)
field(:postalCode, :string)
field(:addressRegion, :string)
field(:streetAddress, :string)
field(:addressCountry, :string)
field(:addressLocality, :string)
end
end
def changeset(struct, data) do
data = fix(data)
struct
|> cast(data, [:type, :name, :longitude, :latitude, :accuracy, :altitude, :radius, :units])
|> cast_embed(:address, with: &address_changeset/2)
|> validate_inclusion(:type, ["Place"])
|> validate_inclusion(:radius, ~w[cm feet inches km m miles])
|> validate_number(:accuracy, greater_than_or_equal_to: 0, less_than_or_equal_to: 100)
|> validate_number(:radius, greater_than_or_equal_to: 0)
|> validate_required([:type, :name])
end
def address_changeset(struct, data) do
struct
|> cast(data, [
:type,
:postalCode,
:addressRegion,
:streetAddress,
:addressCountry,
:addressLocality
])
|> validate_inclusion(:type, ["PostalAddress"])
end
defp fix(data) do
data
|> fix_address()
end
defp fix_address(%{"address" => address} = data) when is_binary(address) do
data
|> Map.put("address", %{
"type" => "PostalAddress",
"streetAddress" => address
})
end
defp fix_address(data), do: data
end

View file

@ -22,6 +22,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Streamer
alias Pleroma.Workers.EventReminderWorker
alias Pleroma.Workers.PollWorker
require Pleroma.Constants
@ -42,23 +43,16 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Sends a notification
@impl true
def handle(
%{
data: %{
"actor" => actor,
"type" => "Accept",
"object" => follow_activity_id
}
} = object,
%{data: %{"actor" => actor, "type" => "Accept", "object" => activity_id}} = object,
meta
) do
with %Activity{actor: follower_id} = follow_activity <-
Activity.get_by_ap_id(follow_activity_id),
%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)
with %Activity{} = activity <-
Activity.get_by_ap_id(activity_id) do
handle_accepted(activity, actor)
if activity.data["type"] === "Join" do
Notification.create_notifications(object)
end
end
{:ok, object, meta}
@ -74,18 +68,14 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
data: %{
"actor" => actor,
"type" => "Reject",
"object" => follow_activity_id
"object" => activity_id
}
} = object,
meta
) do
with %Activity{actor: follower_id} = follow_activity <-
Activity.get_by_ap_id(follow_activity_id),
%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, "reject") do
FollowingRelationship.update(follower, followed, :follow_reject)
Notification.dismiss(follow_activity)
with %Activity{} = activity <-
Activity.get_by_ap_id(activity_id) do
handle_rejected(activity, actor)
end
{:ok, object, meta}
@ -427,6 +417,43 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
end
# Tasks this handles:
# - accepts join if event is local and public
@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" or
object.data["actor"] == joined_event.data["actor"]) do
{:ok, accept_data, _} = Builder.accept(joined_event, object)
{:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true)
end
if Object.local?(joined_event) and joined_event.data["joinMode"] != "free" and
object.data["actor"] != joined_event.data["actor"] do
Utils.update_participation_request_count_in_object(joined_event)
end
Notification.create_notifications(object)
{:ok, object, meta}
end
@impl true
def handle(%{actor: actor_id, data: %{"type" => "Leave", "object" => event_id}} = object, meta) do
with undone_object <- Utils.get_existing_join(actor_id, event_id),
:ok <- handle_undoing(undone_object) do
event = Object.get_by_ap_id(event_id)
if Object.local?(event) and event.data["joinMode"] != "free" do
Utils.update_participation_request_count_in_object(event)
end
{:ok, object, meta}
end
end
# Nothing to do
@impl true
def handle(object, meta) do
@ -472,6 +499,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{:ok, _, updated} =
Object.Updater.do_update_and_invalidate_cache(orig_object, updated_object)
EventReminderWorker.schedule_event_reminder(object)
if updated do
object
|> Activity.normalize()
@ -482,6 +511,52 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{: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 %Object{data: %{"actor" => ^actor}} = joined_event <- Object.get_by_ap_id(event_id),
{:ok, join_activity} <- Utils.update_join_state(join_activity, "accept") do
Utils.add_participation_to_object(join_activity, joined_event)
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),
{:ok, _follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject") do
FollowingRelationship.update(follower, followed, :follow_reject)
Notification.dismiss(follow_activity)
end
end
defp handle_rejected(
%Activity{data: %{"type" => "Join", "object" => event_id}} = join_activity,
actor
) do
with %Object{data: %{"actor" => ^actor}} = 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)
Notification.dismiss(join_activity)
end
end
def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
actor = User.get_cached_by_ap_id(object.data["actor"])
@ -536,8 +611,15 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
end
def handle_object_creation(%{"type" => "Event"} = object, activity, meta) do
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
EventReminderWorker.schedule_event_reminder(activity)
{:ok, object, meta}
end
end
def handle_object_creation(%{"type" => objtype} = object, _activity, meta)
when objtype in ~w[Audio Video Image Event Article Note Page] do
when objtype in ~w[Audio Video Image Article Note Page] do
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
{:ok, object, meta}
end
@ -589,6 +671,16 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
end
def handle_undoing(
%{data: %{"type" => "Join", "actor" => _actor_id, "object" => event_id}} = object
) do
with %Object{} = event_object <- Object.get_by_ap_id(event_id),
{:ok, _} <- Utils.remove_participation_from_object(object, event_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
@spec delete_object(Activity.t()) :: :ok | {:error, Ecto.Changeset.t()}

View file

@ -559,7 +559,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => type} = data,
_options
)
when type in ~w{Update Block Follow Accept Reject} do
when type in ~w{Update Block Follow Accept Reject Join Leave} do
fixed_obj = maybe_fix_object(data["object"])
data = if fixed_obj != nil, do: %{data | "object" => fixed_obj}, else: data
@ -622,7 +622,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
} = data,
_options
)
when type in ["Like", "EmojiReact", "Announce", "Block"] do
when type in ["Like", "EmojiReact", "Announce", "Block", "Join"] do
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
end

View file

@ -448,6 +448,48 @@ defmodule Pleroma.Web.ActivityPub.Utils do
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
def update_participation_request_count_in_object(object) do
params = %{
type: "Join",
object: object.data["id"],
state: "pending"
}
count =
[]
|> ActivityPub.fetch_activities_query(params)
|> Repo.aggregate(:count)
data = Map.put(object.data, "participation_request_count", count)
object
|> Changeset.change(data: data)
|> Object.update_and_set_cache()
end
defp fetch_participations(object) do
if is_list(object.data["participations"]) do
object.data["participations"]
else
[]
end
end
#### Follow-related helpers
@doc """
@ -956,4 +998,26 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Enum.reject(&User.blocks?(&1, poster))
|> Enum.each(&Pleroma.Web.CommonAPI.repeat(activity.id, &1))
end
def get_existing_join(actor, id) do
actor
|> Activity.Queries.by_actor()
|> Activity.Queries.by_object_id(id)
|> Activity.Queries.by_type("Join")
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> 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

View file

@ -140,7 +140,8 @@ defmodule Pleroma.Web.ApiSpec do
"Status actions",
"Media attachments",
"Bookmark folders",
"Tags"
"Tags",
"Event actions"
]
},
%{

View file

@ -136,6 +136,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
BooleanLike.schema(),
"Include only statuses with media attached"
),
Operation.parameter(
:only_events,
:query,
BooleanLike,
"Include only objects with Event type"
),
Operation.parameter(
:with_muted,
:query,

View file

@ -174,6 +174,11 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
"Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.",
nullable: true
},
participation_message: %Schema{
type: :string,
description: "Description of event participation request",
nullable: true
},
pleroma: %Schema{
type: :object,
properties: %{
@ -211,12 +216,17 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
"status",
"update",
"admin.sign_up",
"admin.report"
"admin.report",
"pleroma:participation_accepted",
"pleroma:participation_request",
"pleroma:event_reminder",
"pleroma:event_update"
],
description: """
The type of event that resulted in the notification.
- `follow` - Someone followed you
- `follow_request` - Someone wants to follow you
- `mention` - Someone mentioned you in their status
- `reblog` - Someone boosted one of your statuses
- `favourite` - Someone favourited one of your statuses
@ -229,6 +239,10 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
- `update` - A status you boosted has been edited
- `admin.sign_up` - Someone signed up (optionally sent to admins)
- `admin.report` - A new report has been filed
- `pleroma:event_reminder` An event you are participating in or created is taking place soon
- `pleroma:event_update` An event you are participating in was edited
- `pleroma:participation_request - Someone wants to participate in your event
- `pleroma:participation_accepted - Your event participation request was accepted
"""
}
end

View file

@ -0,0 +1,341 @@
# 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.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.ParticipationRequest
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.StatusOperation
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 update_operation do
%Operation{
tags: ["Event actions"],
summary: "Update event",
description: "Change the content of an event",
operationId: "PleromaAPI.EventController.update",
security: [%{"oAuth" => ["write"]}],
parameters: [id_param()],
requestBody: request_body("Parameters", update_request(), required: true),
responses: %{
200 => event_response(),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def participations_operation do
%Operation{
tags: ["Event actions"],
summary: "Participants list",
description: "View who joined a given event",
operationId: "EventController.participations",
security: [%{"oAuth" => ["read"]}],
parameters: [id_param() | pagination_params()],
responses: %{
200 =>
Operation.response(
"Array of Accounts",
"application/json",
AccountOperation.array_of_accounts()
),
403 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def participation_requests_operation do
%Operation{
tags: ["Event actions"],
summary: "Participation requests list",
description: "View who wants to join the event",
operationId: "EventController.participations",
security: [%{"oAuth" => ["read"]}],
parameters: [id_param() | pagination_params()],
responses: %{
200 =>
Operation.response(
"Array of participation requests",
"application/json",
array_of_participation_requests()
),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def join_operation do
%Operation{
tags: ["Event actions"],
summary: "Participate",
security: [%{"oAuth" => ["write"]}],
description: "Participate in an event",
operationId: "PleromaAPI.EventController.join",
parameters: [id_param()],
requestBody:
request_body(
"Parameters",
%Schema{
type: :object,
properties: %{
account: Account,
participation_message: %Schema{
type: :string,
description: "Why the user wants to participate"
}
}
},
required: false
),
responses: %{
200 => event_response(),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def leave_operation do
%Operation{
tags: ["Event actions"],
summary: "Unparticipate",
security: [%{"oAuth" => ["write"]}],
description: "Delete event participation",
operationId: "PleromaAPI.EventController.leave",
parameters: [id_param()],
responses: %{
200 => event_response(),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def authorize_participation_request_operation do
%Operation{
tags: ["Event actions"],
summary: "Accept participation",
security: [%{"oAuth" => ["write"]}],
description: "Accept event participation request",
operationId: "PleromaAPI.EventController.authorize_participation_request",
parameters: [id_param(), participant_id_param()],
responses: %{
200 => event_response(),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def reject_participation_request_operation do
%Operation{
tags: ["Event actions"],
summary: "Reject participation",
security: [%{"oAuth" => ["write"]}],
description: "Reject event participation request",
operationId: "PleromaAPI.EventController.reject_participation_request",
parameters: [id_param(), participant_id_param()],
responses: %{
200 => event_response(),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def export_ics_operation do
%Operation{
tags: ["Event actions"],
summary: "Export status",
description: "Export event to .ics",
operationId: "PleromaAPI.EventController.export_ics",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [id_param()],
responses: %{
200 =>
Operation.response("Event", "text/calendar; charset=utf-8", %Schema{type: :string}),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def joined_events_operation do
%Operation{
tags: ["Event actions"],
summary: "Joined events",
description: "Get your joined events",
operationId: "PleromaAPI.EventController.joined_events",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [
Operation.parameter(
:state,
:query,
%Schema{type: :string, enum: ["pending", "reject", "accept"]},
"Filter by join state"
)
| pagination_params()
],
responses: %{
200 =>
Operation.response(
"Array of Statuses",
"application/json",
StatusOperation.array_of_statuses()
)
}
}
end
defp create_request do
%Schema{
title: "EventCreateRequest",
type: :object,
properties: %{
name: %Schema{
type: :string,
description: "Name of the event."
},
status: %Schema{
type: :string,
nullable: true,
description: "Text description of the event."
},
banner_id: %Schema{
nullable: true,
type: :string,
description: "Attachment id to be attached as banner."
},
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"]
},
location_id: %Schema{
type: :string,
description: "Location ID from geospatial provider",
nullable: true
}
},
example: %{
"name" => "Example event",
"status" => "No information for now.",
"start_time" => "2022-02-21T22:00:00.000Z",
"end_time" => "2022-02-21T23:00:00.000Z"
}
}
end
defp update_request do
%Schema{
title: "EventUpdateRequest",
type: :object,
properties: %{
name: %Schema{
type: :string,
description: "Name of the event."
},
status: %Schema{
type: :string,
nullable: true,
description: "Text description of the event."
},
banner_id: %Schema{
nullable: true,
type: :string,
description: "Attachment id to be attached as banner."
},
start_time: %Schema{
type: :string,
format: :"date-time",
description: "Start time."
},
end_time: %Schema{
type: :string,
format: :"date-time",
description: "End time."
},
location_id: %Schema{
type: :string,
description: "Location ID from geospatial provider",
nullable: true
}
},
example: %{
"name" => "Updated event",
"status" => "We had to reschedule the event.",
"start_time" => "2022-02-22T22:00:00.000Z",
"end_time" => "2022-02-22T23:00:00.000Z"
}
}
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
defp participant_id_param do
Operation.parameter(:participant_id, :path, FlakeID, "Event participant ID",
example: "9umDrYheeY451cQnEe",
required: true
)
end
def array_of_participation_requests do
%Schema{
title: "ArrayOfParticipationRequests",
type: :array,
items: ParticipationRequest,
example: [ParticipationRequest.schema().example]
}
end
end

View file

@ -0,0 +1,56 @@
# 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.PleromaSearchOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.LocationResult
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def location_operation do
%Operation{
tags: ["Search"],
summary: "Search locations",
security: [%{"oAuth" => []}],
operationId: "PleromaAPI.SearchController.location",
parameters: [
Operation.parameter(
:q,
:query,
%Schema{type: :string},
"What to search for",
required: true
),
Operation.parameter(
:locale,
:query,
%Schema{type: :string},
"The user's locale. Geocoding backends will make use of this value"
),
Operation.parameter(
:type,
:query,
%Schema{type: :string, enum: ["ADMINISTRATIVE"]},
"Filter by type of results"
)
],
responses: %{
200 => Operation.response("Results", "application/json", location_results())
}
}
end
def location_results do
%Schema{
type: :array,
items: LocationResult,
description: "Locations which match the given query",
example: [LocationResult.schema().example]
}
end
end

View file

@ -12,6 +12,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.LocationResult
alias Pleroma.Web.ApiSpec.Schemas.Poll
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
alias Pleroma.Web.ApiSpec.Schemas.Status
@ -544,7 +545,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
responses: %{
200 => status_response(),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
404 => Operation.response("Not Found", "application/json", ApiError),
422 => Operation.response("Unprocessable Entity", "application/json", ApiError)
}
}
end
@ -828,6 +830,11 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
content_type: %Schema{
type: :string,
description: "The content type of the source"
},
location: %Schema{
allOf: [LocationResult],
description: "Location result for an event",
nullable: true
}
}
}

View file

@ -27,6 +27,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
local_param(),
remote_param(),
only_media_param(),
only_events_param(),
with_muted_param(),
exclude_visibilities_param(),
reply_visibility_param() | pagination_params()
@ -62,6 +63,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
local_param(),
instance_param(),
only_media_param(),
only_events_param(),
remote_param(),
with_muted_param(),
exclude_visibilities_param(),
@ -109,6 +111,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
),
local_param(),
only_media_param(),
only_events_param(),
remote_param(),
with_muted_param(),
exclude_visibilities_param() | pagination_params()
@ -139,6 +142,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
local_param(),
remote_param(),
only_media_param(),
only_events_param(),
exclude_visibilities_param() | pagination_params()
],
operationId: "TimelineController.list",
@ -211,6 +215,15 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
)
end
defp only_events_param do
Operation.parameter(
:only_events,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Include only objects with Event type"
)
end
defp remote_param do
Operation.parameter(
:remote,

View file

@ -0,0 +1,112 @@
# 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.Schemas.Event do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Event",
description: "Represents an event attached to a status",
type: :object,
properties: %{
name: %Schema{
type: :string,
description: "Name of the event"
},
start_time: %Schema{
type: :string,
format: :"date-time",
description: "Start time",
nullable: true
},
end_time: %Schema{
type: :string,
format: :"date-time",
description: "End time",
nullable: true
},
join_mode: %Schema{
type: :string,
description: "Who can join the event"
},
participants_count: %Schema{
type: :integer,
description: "Event participants count",
nullable: true
},
participation_request_count: %Schema{
type: :integer,
description: "Event participation requests count",
nullable: true
},
location: %Schema{
type: :object,
description: "Location where the event takes part",
properties: %{
name: %Schema{
type: :string,
description: "Object name",
nullable: true
},
url: %Schema{
type: :string,
description: "Object URL",
nullable: true
},
longitude: %Schema{
type: :number,
description: "Object vertical coordinate",
nullable: true
},
latitude: %Schema{
type: :number,
description: "Object horizontal coordinate",
nullable: true
},
street: %Schema{
type: :string,
description: "Object street",
nullable: true
},
postal_code: %Schema{
type: :string,
description: "Object postal code",
nullable: true
},
locality: %Schema{
type: :string,
description: "Object locality",
nullable: true
},
region: %Schema{
type: :string,
description: "Object region",
nullable: true
},
country: %Schema{
type: :string,
description: "Object country",
nullable: true
}
},
nullable: true
},
join_state: %Schema{
type: :string,
description: "Have you joined the event?",
enum: ["pending", "reject", "accept"],
nullable: true
}
},
example: %{
name: "Example event"
# start_time: "2022-02-21T22:00:00.000Z",
# end_time: "2022-02-21T23:00:00.000Z",
# join_mode: "free",
# participants_count: 0
}
})
end

View file

@ -0,0 +1,80 @@
# 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.Schemas.LocationResult do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "LocationResult",
description: "Represents a location lookup result",
type: :object,
properties: %{
country: %Schema{type: :string, description: "The address's country", nullable: true},
description: %Schema{
type: :string,
description: "The address's description",
nullable: true
},
locality: %Schema{type: :string, description: "The address's locality", nullable: true},
origin_id: %Schema{
type: :string,
description: "The address's original ID from the provider",
nullable: true
},
origin_provider: %Schema{
type: :string,
description: "The provider used by instance",
nullable: true
},
postal_code: %Schema{
type: :string,
description: "The address's postal code",
nullable: true
},
region: %Schema{type: :string, description: "The address's region", nullable: true},
street: %Schema{
type: :string,
description: "The address's street name (with number)",
nullable: true
},
timezone: %Schema{
type: :string,
description: "The (estimated) timezone of the location",
nullable: true
},
type: %Schema{type: :string, description: "The address's type", nullable: true},
url: %Schema{type: :string, description: "The address's URL", nullable: true},
geom: %Schema{
type: :object,
properties: %{
coordinates: %Schema{
type: :array,
items: %Schema{type: :number}
},
srid: %Schema{type: :integer}
},
nullable: true
}
},
example: %{
"country" => "Poland",
"description" => "Dworek Modrzewiowy",
"geom" => %{
"coordinates" => [19.35267765039501, 52.233616299999994],
"srid" => 4326
},
"locality" => "Kutno",
"origin_id" => "251399743",
"origin_provider" => "nominatim",
"postal_code" => "80-549",
"region" => "Łódź Voivodeship",
"street" => "20 Gabriela Narutowicza",
"timezone" => nil,
"type" => "house",
"url" => nil
}
})
end

View file

@ -0,0 +1,30 @@
# 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.Schemas.ParticipationRequest do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
require OpenApiSpex
OpenApiSpex.schema(%{
title: "ParticipationRequest",
description: "Represents an event participation request",
type: :object,
properties: %{
account: %Schema{
allOf: [Account],
description: "The account that wants to participate in the event."
},
participation_message: %Schema{
type: :string,
description: "Why the user wants to participate"
}
},
example: %{
"account" => Account.schema().example,
"participation_message" => "I'm interested in this event"
}
})
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.Attachment
alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.Event
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Poll
alias Pleroma.Web.ApiSpec.Schemas.Tag
@ -255,6 +256,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
nullable: true,
description:
"The ID of the list the post is addressed to (if any, only returned to author)"
},
event: %Schema{
allOf: [Event],
nullable: true,
description: "The event attached to the status"
}
}
},

View file

@ -362,6 +362,99 @@ defmodule Pleroma.Web.CommonAPI do
end
end
def join(%User{} = user, event_id, params \\ %{}) do
participation_message = Map.get(params, :participation_message)
case join_helper(user, event_id, participation_message) do
{:ok, _} = res ->
res
{:error, :not_found} = res ->
res
{:error, :external_joins} ->
{:error, dgettext("errors", "Joins are managed by external system")}
{:error, :not_an_event} ->
{:error, dgettext("errors", "Not an event")}
{:error, e} ->
Logger.error("Could not join #{event_id}. Error: #{inspect(e, pretty: true)}")
{:error, dgettext("errors", "Could not join")}
end
end
defp join_helper(user, id, participation_message) do
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
{_, true} <- {:object_type, object.data["type"] == "Event"},
{_, true} <- {:managed_joins, object.data["joinMode"] != "external"},
{_, {:ok, join_object, meta}} <-
{:build_object, Builder.join(user, object, participation_message)},
{_, {: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}
{:object_type, false} ->
{:error, :not_an_event}
{:managed_joins, false} ->
{:error, :external_joins}
{: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
def leave(%User{ap_id: participant_ap_id} = user, event_id) do
with %Activity{data: %{"object" => event_ap_id}} <- Activity.get_by_id(event_id),
%Activity{} = join_activity <- Utils.get_existing_join(participant_ap_id, event_ap_id),
{:ok, undo, _} <- Builder.undo(user, join_activity),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else
nil ->
{:error, dgettext("errors", "Not participating in the event")}
_ ->
{:error, dgettext("errors", "Could not remove join activity")}
end
end
def accept_join_request(%User{} = user, %User{ap_id: participant_ap_id} = participant, event_id) do
with %Activity{} = join_activity <- Utils.get_existing_join(participant_ap_id, event_id),
{:ok, accept_data, _} <- Builder.accept(user, join_activity),
{:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true),
event <- Object.get_by_ap_id(event_id) do
if Object.local?(event) and event.data["joinMode"] != "free" and
join_activity.data["actor"] == event.data["actor"] do
Utils.update_participation_request_count_in_object(event)
end
{:ok, participant}
end
end
def reject_join_request(%User{} = user, %User{ap_id: participant_ap_id} = participant, event_id) do
with %Activity{} = join_activity <- Utils.get_existing_join(participant_ap_id, event_id),
{:ok, reject_data, _} <- Builder.reject(user, join_activity),
{:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true),
event <- Object.get_by_ap_id(event_id),
{:ok, _} <- Utils.update_participation_request_count_in_object(event) do
{:ok, participant}
end
end
defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
do: {:error, dgettext("errors", "Poll's author can't vote")}
@ -720,6 +813,47 @@ defmodule Pleroma.Web.CommonAPI do
end
end
def event(user, data, location \\ nil) do
with {:ok, draft} <- ActivityDraft.event(user, data, location) do
ActivityPub.create(draft.changes)
end
end
def update_event(user, orig_activity, changes, location \\ nil) do
with orig_object <- Object.normalize(orig_activity),
{:ok, new_object} <- make_update_event_data(user, orig_object, changes, location),
{:ok, update_data, _} <- Builder.update(user, new_object),
{:ok, update, _} <- Pipeline.common_pipeline(update_data, local: true) do
{:ok, update}
else
_ -> {:error, nil}
end
end
defp make_update_event_data(user, orig_object, changes, location) do
kept_params = %{
visibility: Visibility.get_visibility(orig_object),
in_reply_to_id:
with replied_id when is_binary(replied_id) <- orig_object.data["inReplyTo"],
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(replied_id) do
activity_id
else
_ -> nil
end
}
params = Map.merge(changes, kept_params)
with {:ok, draft} <- ActivityDraft.event(user, params, location) do
change =
Object.Updater.make_update_object_data(orig_object.data, draft.object, Utils.make_date())
{:ok, change}
else
_ -> {:error, nil}
end
end
defp maybe_cancel_jobs(%Activity{id: activity_id}) do
Oban.Job
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
alias Pleroma.Conversation.Participation
alias Pleroma.Language.LanguageDetector
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
@ -45,7 +46,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
language: nil,
object: nil,
preview?: false,
changes: %{}
changes: %{},
location: nil,
start_time: nil,
end_time: nil,
location_id: nil,
location_provider: nil
def new(user, params) do
%__MODULE__{user: user}
@ -101,6 +107,41 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
%__MODULE__{draft | object: object}
end
@spec event(any, map) :: {:error, any} | {:ok, %{:valid? => true, optional(any) => any}}
def event(user, params, location \\ nil) do
user
|> new(params)
|> status()
|> visibility()
|> content()
|> to_and_cc()
|> context()
|> with_valid(&event_banner/1)
|> event_location(location)
|> with_valid(&event_date/1)
|> event_object()
|> with_valid(&changes/1)
|> validate()
end
defp event_object(draft) do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
{:ok, event_data, _meta} = Builder.event(draft)
object =
event_data
|> Map.put("emoji", emoji)
|> Map.put("source", %{
"content" => draft.status,
"mediaType" => Utils.get_content_type(draft.params[:content_type])
})
|> Map.put("generator", draft.params[:generator])
|> Map.put("content_type", draft.params[:content_type])
%__MODULE__{draft | object: object}
end
defp put_params(draft, params) do
params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
%__MODULE__{draft | params: params}
@ -339,6 +380,78 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
%__MODULE__{draft | changes: changes}
end
defp event_date(draft) do
case draft.params[:start_time] do
%DateTime{} = start_time ->
case draft.params[:end_time] do
%DateTime{} = end_time ->
if DateTime.compare(end_time, start_time) == :lt do
add_error(draft, dgettext("errors", "Event can't end before its start"))
else
start_time = start_time |> DateTime.to_iso8601()
end_time = end_time |> DateTime.to_iso8601()
%__MODULE__{draft | start_time: start_time, end_time: end_time}
end
_ ->
start_time = start_time |> DateTime.to_iso8601()
%__MODULE__{draft | start_time: start_time}
end
_ ->
add_error(draft, dgettext("errors", "Start date is required"))
end
end
defp event_location(draft, %Geospatial.Address{} = address) do
location = %{
"type" => "Place",
"name" => address.description,
"id" => address.url,
"address" => %{
"type" => "PostalAddress",
"streetAddress" => address.street,
"postalCode" => address.postal_code,
"addressLocality" => address.locality,
"addressRegion" => address.region,
"addressCountry" => address.country
}
}
location =
if is_nil(address.geom) do
location
else
{longitude, latitude} = address.geom.coordinates
location
|> Map.put("longitude", longitude)
|> Map.put("latitude", latitude)
end
%__MODULE__{
draft
| location: location,
location_id: address.origin_id,
location_provider: address.origin_provider
}
end
defp event_location(draft, _), do: draft
defp event_banner(draft) do
with media_id when is_binary(media_id) <- draft.params[:banner_id],
%Object{data: data} <- Repo.get(Object, media_id) do
banner = Map.put(data, "name", "Banner")
%__MODULE__{draft | attachments: [banner]}
else
_ -> draft
end
end
defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
defp with_valid(draft, _func), do: draft

View file

@ -437,6 +437,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_notify_followers(recipients, _), do: recipients
def maybe_notify_participants(
recipients,
%Activity{data: %{"type" => "Update"}} = activity
) do
with %Object{data: object} <- Object.normalize(activity, fetch: false) do
participant_ids = Map.get(object, "participations", [])
recipients ++ participant_ids
else
_e -> recipients
end
end
def maybe_notify_participants(recipients, _), do: recipients
def maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end)

View file

@ -35,6 +35,10 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
poll
update
status
pleroma:participation_request
pleroma:participation_accepted
pleroma:event_reminder
pleroma:event_update
}
# GET /api/v1/notifications

View file

@ -23,7 +23,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
@ -104,6 +103,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
plug(Pleroma.Web.Plugs.SetApplicationPlug, [] when action in [:create, :update])
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
@ -279,6 +280,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
{_, true} <- {:is_create, activity.data["type"] == "Create"},
actor <- Activity.user_actor(activity),
{_, true} <- {:own_status, actor.id == user.id},
{_, true} <- {:not_event, activity.object.data["type"] != "Event"},
changes <- body_params |> put_application(conn),
{_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(activity, user, changes)},
{_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
@ -290,6 +292,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
)
else
{:own_status, _} -> {:error, :forbidden}
{:not_event, _} -> {:error, :unprocessable_entity, "Use event update route"}
{:pipeline, _} -> {:error, :internal_server_error}
_ -> {:error, :not_found}
end
@ -626,13 +629,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
)
end
defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
if user.disclose_client do
%{client_name: client_name, website: website} = Repo.preload(token, :app).app
Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
else
Map.put(params, :generator, nil)
end
defp put_application(params, %{assigns: %{application: application}} = _conn) do
Map.put(params, :generator, application)
end
defp put_application(params, _), do: Map.put(params, :generator, nil)

View file

@ -157,7 +157,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
"pleroma:bookmark_folders",
if Pleroma.Language.LanguageDetector.configured?() do
"pleroma:language_detection"
end
end,
"pleroma:events"
]
|> Enum.filter(& &1)
end

View file

@ -106,7 +106,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
}
case notification.type do
"mention" ->
type when type in ["mention", "poll", "pleroma:event_reminder"] ->
put_status(response, activity, reading_user, status_render_opts)
"status" ->
@ -118,15 +118,12 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
"reblog" ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
"update" ->
type when type in ["update", "pleroma:event_update"] ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
"move" ->
put_target(response, activity, reading_user, %{})
"poll" ->
put_status(response, activity, reading_user, status_render_opts)
"pleroma:emoji_reaction" ->
response
|> put_status(parent_activity_fn.(), reading_user, status_render_opts)
@ -138,6 +135,21 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
"pleroma:report" ->
put_report(response, activity)
"pleroma:participation_accepted" ->
request_activity = Activity.get_by_ap_id(activity.data["object"])
create_activity = Activity.get_create_by_object_ap_id(request_activity.data["object"])
response
|> put_status(create_activity, reading_user, status_render_opts)
|> put_participation_request(request_activity)
"pleroma:participation_request" ->
create_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
response
|> put_status(create_activity, reading_user, status_render_opts)
|> put_participation_request(activity)
type when type in ["follow", "follow_request"] ->
response
end
@ -180,4 +192,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
Map.put(response, :target, target_render)
end
defp put_participation_request(response, activity) do
Map.put(response, :participation_message, activity.data["participationMessage"])
end
end

View file

@ -466,7 +466,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
pinned_at: pinned_at,
quotes_count: object.data["quotesCount"] || 0,
bookmark_folder: bookmark_folder,
list_id: get_list_id(object, client_posted_this_activity)
list_id: get_list_id(object, client_posted_this_activity),
event: build_event(object.data, opts[:for])
}
}
end
@ -565,7 +566,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
id: activity.id,
text: get_source_text(Map.get(object.data, "source", "")),
spoiler_text: Map.get(object.data, "summary", ""),
content_type: get_source_content_type(object.data["source"])
content_type: get_source_content_type(object.data["source"]),
location: build_source_location(object.data)
}
end
@ -602,7 +604,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
href = attachment_url["href"] |> MediaProxy.url()
href_remote = attachment_url["href"]
href = href_remote |> MediaProxy.url()
href_preview = attachment_url["href"] |> MediaProxy.preview_url()
meta = render("attachment_meta.json", %{attachment: attachment})
@ -641,7 +644,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
%{
id: attachment_id,
url: href,
remote_url: href,
remote_url: href_remote,
preview_url: href_preview,
text_url: href,
type: type,
@ -727,7 +730,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end
def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
def render_content(%{data: %{"name" => name, "type" => type}} = object)
when not is_nil(name) and name != "" and type != "Event" do
url = object.data["url"] || object.data["id"]
"<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
@ -784,6 +788,61 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end)
end
defp build_event(%{"type" => "Event"} = data, for_user) do
%{
name: data["name"],
start_time: data["startTime"],
end_time: data["endTime"],
join_mode: data["joinMode"],
participants_count: data["participation_count"],
location: build_event_location(data["location"]),
join_state: build_event_join_state(for_user, data["id"]),
participation_request_count: maybe_put_participation_request_count(data, for_user)
}
end
defp build_event(_, _), do: nil
defp build_event_location(%{"type" => "Place"} = location) do
%{
name: location["name"],
url: location["url"],
longitude: location["longitude"],
latitude: location["latitude"]
}
|> maybe_put_address(location["address"])
end
defp build_event_location(_), do: nil
defp maybe_put_address(location, %{"type" => "PostalAddress"} = address) do
Map.merge(location, %{
street: address["streetAddress"],
postal_code: address["postalCode"],
locality: address["addressLocality"],
region: address["addressRegion"],
country: address["addressCountry"]
})
end
defp maybe_put_address(location, _), do: location
defp build_event_join_state(%{ap_id: actor}, id) do
latest_join = Pleroma.Web.ActivityPub.Utils.get_existing_join(actor, id)
if latest_join do
latest_join.data["state"]
end
end
defp build_event_join_state(_, _), do: nil
defp maybe_put_participation_request_count(%{"actor" => actor} = data, %{ap_id: actor}) do
data["participation_request_count"]
end
defp maybe_put_participation_request_count(_, _), do: nil
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
@ -841,6 +900,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp get_language(object), do: object.data["language"]
def build_source_location(%{"location_id" => location_id}) when is_binary(location_id) do
location = Geospatial.Service.service().get_by_id(location_id) |> List.first()
if location do
Pleroma.Web.PleromaAPI.SearchView.render("show_location.json", %{location: location})
else
nil
end
end
def build_source_location(_), do: nil
defp proxied_url(url, page_url_data) do
if is_binary(url) do
build_image_url(URI.parse(url), page_url_data) |> MediaProxy.url()

View file

@ -0,0 +1,310 @@
# 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: [add_link_headers: 2, json_response: 3, try_render: 3]
require Ecto.Query
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.PleromaAPI.EventView
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
:assign_participant
when action in [:authorize_participation_request, :reject_participation_request]
)
plug(
:assign_event_activity
when action in [
:participations,
:participation_requests,
:authorize_participation_request,
:reject_participation_request,
:join,
:leave,
:export_ics
]
)
plug(
OAuthScopesPlug,
%{scopes: ["write"]}
when action in [
:create,
:update,
:authorize_participation_request,
:reject_participation_request,
:join,
:leave
]
)
plug(
OAuthScopesPlug,
%{scopes: ["read"]}
when action in [:participations, :participation_requests, :joined_events]
)
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
when action in [:export_ics]
)
@rate_limited_event_actions ~w(create update join leave)a
plug(
RateLimiter,
[name: :status_id_action, bucket_name: "status_id_action:join_leave", params: [:id]]
when action in ~w(join leave)a
)
plug(RateLimiter, [name: :events_actions] when action in @rate_limited_event_actions)
plug(Pleroma.Web.Plugs.SetApplicationPlug, [] when action in [:create, :update])
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEventOperation
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
params =
params
|> Map.put(:status, Map.get(params, :status, ""))
with location <- get_location(params),
{:ok, activity} <- CommonAPI.event(user, params, location) 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
@doc "PUT /api/v1/pleroma/events/:id"
def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
{_, true} <- {:is_create, activity.data["type"] == "Create"},
actor <- Activity.user_actor(activity),
{_, true} <- {:own_status, actor.id == user.id},
changes <- body_params |> Map.put(:generator, conn.assigns.application),
location <- get_location(body_params),
{_, {:ok, _update_activity}} <-
{:pipeline, CommonAPI.update_event(user, activity, changes, location)},
{_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
conn
|> put_view(StatusView)
|> try_render("show.json",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_muted: Map.get(params, :with_muted, false)
)
else
{:own_status, _} -> {:error, :forbidden}
{:pipeline, _} -> {:error, :internal_server_error}
_ -> {:error, :not_found}
end
end
defp get_location(%{location_id: location_id}) when is_binary(location_id) do
result = Geospatial.Service.service().get_by_id(location_id)
result |> List.first()
end
defp get_location(_), do: nil
def participations(%{assigns: %{user: user, event_activity: activity}} = conn, params) do
with %Object{data: %{"participations" => participations}} <-
Object.normalize(activity, fetch: false) do
users =
User
|> Ecto.Query.where([u], u.ap_id in ^participations)
|> Pagination.fetch_paginated(params)
|> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
|> render("index.json", for: user, users: users, as: :user)
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end
def participation_requests(
%{assigns: %{user: %{ap_id: user_ap_id} = for_user, event_activity: activity}} = conn,
params
) do
case activity do
%Activity{actor: ^user_ap_id, data: %{"object" => ap_id}} ->
params =
Map.merge(params, %{
type: "Join",
object: ap_id,
state: "pending",
skip_preload: true
})
activities =
[]
|> ActivityPub.fetch_activities_query(params)
|> Pagination.fetch_paginated(params)
conn
|> add_link_headers(activities)
|> put_view(EventView)
|> render("participation_requests.json",
activities: activities,
for: for_user,
as: :activity
)
%Activity{} ->
render_error(conn, :forbidden, "Can't get participation requests")
{:error, error} ->
json_response(conn, :bad_request, %{error: error})
end
end
def join(%{assigns: %{user: %{ap_id: actor}, event_activity: %{actor: actor}}} = conn, _) do
render_error(conn, :bad_request, "Can't join your own event")
end
def join(
%{assigns: %{user: user, event_activity: activity}, body_params: params} = conn,
_
) do
with {:ok, _} <- CommonAPI.join(user, activity.id, params) do
conn
|> put_view(StatusView)
|> try_render("show.json", activity: activity, for: user, as: :activity)
end
end
def leave(
%{assigns: %{user: %{ap_id: actor}, event_activity: %{actor: actor}}} = conn,
_
) do
render_error(conn, :bad_request, "Can't leave your own event")
end
def leave(%{assigns: %{user: user, event_activity: activity}} = conn, _) do
with {:ok, _} <- CommonAPI.leave(user, activity.id) do
conn
|> put_view(StatusView)
|> try_render("show.json", activity: activity, for: user, as: :activity)
else
{:error, error} ->
json_response(conn, :bad_request, %{error: error})
end
end
def authorize_participation_request(
%{
assigns: %{
user: for_user,
participant: participant,
event_activity: %Activity{data: %{"object" => ap_id}} = activity
}
} = conn,
_
) do
with actor <- Activity.user_actor(activity),
{_, true} <- {:own_event, actor.id == for_user.id},
{:ok, _} <- CommonAPI.accept_join_request(for_user, participant, ap_id) do
conn
|> put_view(StatusView)
|> try_render("show.json", activity: activity, for: for_user, as: :activity)
else
{:own_event, _} -> {:error, :forbidden}
end
end
def reject_participation_request(
%{
assigns: %{
user: for_user,
participant: participant,
event_activity: %Activity{data: %{"object" => ap_id}} = activity
}
} = conn,
_
) do
with actor <- Activity.user_actor(activity),
{_, true} <- {:own_event, actor.id == for_user.id},
{:ok, _} <- CommonAPI.reject_join_request(for_user, participant, ap_id) do
conn
|> put_view(StatusView)
|> try_render("show.json", activity: activity, for: for_user, as: :activity)
else
{:own_event, _} -> {:error, :forbidden}
end
end
def export_ics(%{assigns: %{event_activity: activity}} = conn, _) do
render(conn, "show.ics", activity: activity)
end
defp assign_participant(%{params: %{participant_id: id}} = conn, _) do
case User.get_cached_by_id(id) do
%User{} = participant -> assign(conn, :participant, participant)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
end
defp assign_event_activity(%{assigns: %{user: user}, params: %{id: event_id}} = conn, _) do
with %Activity{} = activity <- Activity.get_by_id(event_id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)} do
assign(conn, :event_activity, activity)
else
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
end
def joined_events(%{assigns: %{user: %User{} = user}} = conn, params) do
activities = ActivityPub.fetch_joined_events(user, params)
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json",
activities: activities,
for: user,
as: :activity
)
end
end

View file

@ -0,0 +1,23 @@
# 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.SearchController do
use Pleroma.Web, :controller
alias Pleroma.Web.Plugs.OAuthScopesPlug
require Pleroma.Constants
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: [], op: :&} when action in [:location])
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaSearchOperation
def location(conn, %{q: query} = params) do
result = Geospatial.Service.service().search(query, params |> Map.to_list())
render(conn, "index_locations.json", locations: result)
end
end

View file

@ -0,0 +1,86 @@
# 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.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
def render("participation_requests.json", %{activities: activities} = opts) do
render_many(
activities,
__MODULE__,
"participation_request.json",
Map.delete(opts, :activities)
)
end
def render("participation_request.json", %{activity: activity} = opts) do
user = CommonAPI.get_user(activity.data["actor"])
%{
account:
AccountView.render("show.json", %{
user: user,
for: opts[:for]
}),
participation_message: activity.data["participationMessage"]
}
end
def render("show.ics", %{activity: %Activity{actor: actor_ap_id} = activity}) do
with %Object{} = object <- Object.normalize(activity, fetch: false),
%User{} = user <- User.get_cached_by_ap_id(actor_ap_id) do
event = %ICalendar.Event{
summary: object.data["name"],
dtstart: object.data["startTime"] |> get_date,
dtend: object.data["endTime"] |> get_date,
description: Pleroma.HTML.strip_tags(object.data["content"]),
uid: object.id,
url: object.data["url"] || object.data["id"],
geo: get_coords(object),
location: get_location(object),
organizer: Pleroma.HTML.strip_tags(user.name || user.nickname)
}
%ICalendar{events: [event]}
end
end
defp get_coords(%Object{
data: %{"location" => %{"longitude" => longitude, "latitude" => latitude}}
}) do
{latitude, longitude}
end
defp get_coords(_) do
nil
end
defp get_location(%Object{
data: %{"location" => %{"name" => description, "address" => %{} = address}}
}) do
String.trim(
"#{description} #{address["streetAddress"]} #{address["postalCode"]} #{address["addressLocality"]} #{address["addressRegion"]} #{address["addressCountry"]}"
)
end
defp get_location(_) do
nil
end
defp get_date(date) when is_binary(date) do
{:ok, date, _} = DateTime.from_iso8601(date)
date
end
defp get_date(_) do
nil
end
end

View file

@ -0,0 +1,36 @@
# 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.SearchView do
use Pleroma.Web, :view
def render("index_locations.json", %{locations: locations}) do
render_many(locations, __MODULE__, "show_location.json", as: :location)
end
def render("show_location.json", %{location: location}) do
%{
url: location.url,
description: location.description,
geom: render("geom.json", %{geom: location.geom}),
country: location.country,
locality: location.locality,
region: location.region,
postal_code: location.postal_code,
street: location.street,
origin_id: "#{location.origin_id}",
origin_provider: location.origin_provider,
type: location.type,
timezone: location.timezone
}
end
def render("geom.json", %{
geom: %Geo.Point{coordinates: {longitude, latitude}, properties: _properties, srid: srid}
}) do
%{coordinates: [longitude, latitude], srid: srid}
end
def render("geom.json", %{geom: _}), do: nil
end

View file

@ -0,0 +1,28 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.SetApplicationPlug do
import Plug.Conn, only: [assign: 3]
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
def init(_), do: nil
def call(conn, _) do
assign(conn, :application, get_application(conn))
end
defp get_application(%{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
if user.disclose_client do
%{client_name: client_name, website: website} = Repo.preload(token, :app).app
%{type: "Application", name: client_name, url: website}
else
nil
end
end
defp get_application(_), do: nil
end

View file

@ -602,6 +602,30 @@ defmodule Pleroma.Web.Router do
post("/bookmark_folders", BookmarkFolderController, :create)
patch("/bookmark_folders/:id", BookmarkFolderController, :update)
delete("/bookmark_folders/:id", BookmarkFolderController, :delete)
post("/events", EventController, :create)
get("/events/joined_events", EventController, :joined_events)
put("/events/:id", EventController, :update)
get("/events/:id/participations", EventController, :participations)
get("/events/:id/participation_requests", EventController, :participation_requests)
post(
"/events/:id/participation_requests/:participant_id/authorize",
EventController,
:authorize_participation_request
)
post(
"/events/:id/participation_requests/:participant_id/reject",
EventController,
:reject_participation_request
)
post("/events/:id/join", EventController, :join)
post("/events/:id/leave", EventController, :leave)
get("/events/:id/ics", EventController, :export_ics)
get("/search/location", SearchController, :location)
end
scope [] do

View file

@ -0,0 +1,74 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.EventReminderWorker do
@moduledoc """
Generates notifications for upcoming events.
"""
use Oban.Worker, queue: :background
import Ecto.Query
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
@impl Oban.Worker
def perform(%Job{args: %{"op" => "event_reminder", "activity_id" => activity_id}}) do
with %Activity{} = activity <- find_event_activity(activity_id) do
Notification.create_event_notifications(activity)
end
end
defp find_event_activity(activity_id) do
with nil <- Activity.get_by_id(activity_id) do
{:error, :event_activity_not_found}
end
end
def schedule_event_reminder(%Activity{data: %{"type" => "Create"}, id: activity_id} = activity) do
with %Object{data: %{"type" => "Event", "startTime" => start_time}} <-
Object.normalize(activity),
{:ok, start_time, _} <- DateTime.from_iso8601(start_time),
:gt <-
DateTime.compare(
start_time |> DateTime.add(60 * 60 * -2, :second),
DateTime.utc_now()
) do
%{
op: "event_reminder",
activity_id: activity_id
}
|> new(scheduled_at: start_time |> DateTime.add(60 * 60 * -2, :second))
|> Oban.insert()
else
_ -> {:error, activity}
end
end
def schedule_event_reminder(
%Activity{data: %{"type" => "Update", "object" => %{"id" => ap_id}}} = activity
) do
with %Activity{id: activity_id} = create_activity <-
Activity.get_create_by_object_ap_id(ap_id),
{:ok, _} <- remove_event_reminders(activity_id) do
schedule_event_reminder(create_activity)
else
_ -> {:error, activity}
end
end
def schedule_event_reminder(activity) do
{:error, activity}
end
defp remove_event_reminders(activity_id) do
from(j in Oban.Job,
where: j.state == "scheduled",
where: j.queue == "event_reminders",
where: fragment("?->>'activity_id' = ?", j.args, ^activity_id)
)
|> Oban.cancel_all_jobs()
end
end

View file

@ -207,6 +207,8 @@ defmodule Pleroma.Mixfile do
{:oban_live_dashboard, "~> 0.1.1"},
{:multipart, "~> 0.4.0", optional: true},
{:argon2_elixir, "~> 4.0"},
{:icalendar, "~> 1.1"},
{:geospatial, "~> 0.3.1"},
## dev & test
{:phoenix_live_reload, "~> 1.3.3", only: :dev},

View file

@ -57,6 +57,8 @@
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
"floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"geo": {:hex, :geo, "3.6.0", "00c9c6338579f67e91cd5950af4ae2eb25cdce0c3398718c232539f61625d0bd", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "1dbdebf617183b54bc3c8ad7a36531a9a76ada8ca93f75f573b0ae94006168da"},
"geospatial": {:hex, :geospatial, "0.3.1", "0c8ca9746e44382a43eddd7b353af9aad7c0c5acd0edc78c765ec2566ab7b891", [:mix], [{:geo, "~> 3.6.0", [hex: :geo, repo: "hexpm", optional: false]}, {:hackney, "~> 1.20.1", [hex: :hackney, repo: "hexpm", optional: false]}, {:tesla, "~> 1.11.0", [hex: :tesla, repo: "hexpm", optional: false]}, {:tz_world, "~> 1.3.2", [hex: :tz_world, repo: "hexpm", optional: false]}], "hexpm", "ef5725a7f39551eb43986790077d10065720c020c32f16f60a86f8278c7697e3"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"},
"hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [:rebar3], [{:certifi, "~>2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"},
@ -64,6 +66,7 @@
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"icalendar": {:hex, :icalendar, "1.1.2", "5d0afff5d0143c5bd43f18ae32a777bf0fb9a724543ab05229a460d368f0a5e7", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2060f8e353fdf3047e95a3f012583dc3c0bbd7ca1010e32ed9e9fc5760ad4292"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
@ -81,7 +84,7 @@
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
"mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"},
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"},
"mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"},
@ -139,11 +142,12 @@
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.0", "b583c3f18508f5c5561b674d16cf5d9afd2ea3c04505b7d92baaeac93c1b8260", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "9cba950e1c4733468efbe3f821841f34ac05d28e7af7798622f88ecdbbe63ea3"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"tesla": {:hex, :tesla, "1.11.0", "81b2b10213dddb27105ec6102d9eb0cc93d7097a918a0b1594f2dfd1a4601190", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b83ab5d4c2d202e1ea2b7e17a49f788d49a699513d7c4f08f2aef2c281be69db"},
"tesla": {:hex, :tesla, "1.11.2", "24707ac48b52f72f88fc05d242b1c59a85d1ee6f16f19c312d7d3419665c9cd5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c549cd03aec6a7196a641689dd378b799e635eb393f689b4bd756f750c7a4014"},
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
"timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tz_world": {:hex, :tz_world, "1.3.3", "6d847a8f24d84f091d3385769dad96a27170e8e9a03f5ded9fd86299a99c67b1", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:geo, "~> 1.0 or ~> 2.0 or ~> 3.3", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae9f255954c767fa4e36fa68b2927310a7192b525e10f860a6f4656aab23746"},
"tzdata": {:hex, :tzdata, "1.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"},
"ueberauth": {:hex, :ueberauth, "0.10.7", "5a31cbe11e7ce5c7484d745dc9e1f11948e89662f8510d03c616de03df581ebd", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "0bccf73e2ffd6337971340832947ba232877aa8122dba4c95be9f729c8987377"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},

View file

@ -36,8 +36,7 @@ defmodule Pleroma.Repo.Migrations.AddStatusToNotificationsEnum do
'reblog',
'favourite',
'pleroma:report',
'poll',
'update'
'poll'
)
"""
|> execute()

View file

@ -37,7 +37,8 @@ defmodule Pleroma.Repo.Migrations.AddUpdateToNotificationsEnum do
'reblog',
'favourite',
'pleroma:report',
'poll'
'poll',
'status'
)
"""
|> execute()

View file

@ -0,0 +1,67 @@
defmodule Pleroma.Repo.Migrations.AddPleromaParticipationAcceptedToNotificationsEnum do
use Ecto.Migration
@disable_ddl_transaction true
def up do
"""
alter type notification_type add value 'pleroma:participation_accepted'
"""
|> execute()
"""
alter type notification_type add value 'pleroma:participation_request'
"""
|> execute()
"""
alter type notification_type add value 'pleroma:event_reminder'
"""
|> execute()
"""
alter type notification_type add value 'pleroma:event_update'
"""
|> execute()
end
def down do
alter table(:notifications) do
modify(:type, :string)
end
"""
delete from notifications where type in ('pleroma:participation_accepted', 'pleroma:participation_request', 'pleroma:event_reminder', 'pleroma:event_update')
"""
|> execute()
"""
drop type if exists notification_type
"""
|> execute()
"""
create type notification_type as enum (
'follow',
'follow_request',
'mention',
'move',
'pleroma:emoji_reaction',
'pleroma:chat_mention',
'reblog',
'favourite',
'pleroma:report',
'poll',
'status',
'update'
)
"""
|> execute()
"""
alter table notifications
alter column type type notification_type using (type::notification_type)
"""
|> execute()
end
end

View file

@ -43,7 +43,29 @@
"vcard": "http://www.w3.org/2006/vcard/ns#",
"formerRepresentations": "litepub:formerRepresentations",
"sm": "http://smithereen.software/ns#",
"nonAnonymous": "sm:nonAnonymous"
"nonAnonymous": "sm:nonAnonymous",
"mz": "https://joinmobilizon.org/ns#",
"joinMode": {
"@id": "mz:joinMode",
"@type": "mz:joinModeType"
},
"joinModeType": {
"@id": "mz:joinModeType",
"@type": "rdfs:Class"
},
"participationMessage": {
"@id": "mz:participationMessage",
"@type": "schema:Text"
},
"streetAddress": "schema:streetAddress",
"postalCode": "schema:postalCode",
"addressLocality": "schema:addressLocality",
"addressRegion": "schema:addressRegion",
"addressCountry": "schema:addressCountry",
"location": {
"@id": "schema:location",
"@type": "schema:Place"
}
}
]
}

View file

@ -0,0 +1 @@
{"id":"https://demo.gancio.org/federation/m/1","name":"Demo event","url":"https://demo.gancio.org/event/demo-event","type":"Event","startTime":"2021-07-14T17:30:57.000+02:00","endTime":"2021-07-14T18:30:57.000+02:00","location":{"type":"Place","name":"Colosseo","address":"Piazza del Colosseo, Rome","latitude":null,"longitude":null},"attachment":[],"tag":[{"type":"Hashtag","name":"#test","href":"https://demo.gancio.org/tag/test"}],"published":"2021-07-01T22:33:36.543Z","attributedTo":"https://demo.gancio.org/federation/u/customized","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://demo.gancio.org/federation/u/customized/followers"],"content":"","summary":"Colosseo, Wednesday, 14 July (17:30)","@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"toot":"http://joinmastodon.org/ns#","schema":"http://schema.org#","ProperyValue":"schema:PropertyValue","value":"schema:value","discoverable":"toot:discoverable","Hashtag":"https://www.w3.org/ns/activitystreams#Hashtag","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","focalPoint":{"@container":"@list","@id":"toot:focalPoint"}}]}

View file

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"toot":"http://joinmastodon.org/ns#","schema":"http://schema.org#","ProperyValue":"schema:PropertyValue","value":"schema:value","discoverable":"toot:discoverable","indexable":"toot:indexable"}],"id":"https://demo.gancio.org/federation/u/customized","type":"Application","summary":"Demo instance, you can login with admin@gancio.org / password","name":"customized","preferredUsername":"customized","inbox":"https://demo.gancio.org/federation/u/customized/inbox","outbox":"https://demo.gancio.org/federation/u/customized/outbox","manuallyApprovesFollowers":false,"endpoints":{"sharedInbox":"https://demo.gancio.org/federation/u/customized/inbox"},"discoverable":true,"indexable":true,"attachment":[{"type":"PropertyValue","name":"Website","value":"<a href='https://demo.gancio.org'>https://demo.gancio.org</a>"},{"type":"PropertyValue","name":"Place","value":"Demo"}],"icon":{"type":"Image","mediaType":"image/png","url":"https://demo.gancio.org/logo.png"},"publicKey":{"id":"https://demo.gancio.org/federation/u/customized#main-key","owner":"https://demo.gancio.org/federation/u/customized","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxp9BQ8TvVqu+0xXk7VuZ\nnuO42cHxVI+z/3TQ80AfX5aoUnK/uP7lIPy+NiIgRRu0L4hsjEs+HP6Ny9NAKFtC\nddS3pUrgIDz/AUyKeYRsCycw4XyeX7gaqIan4vCx+ANPDVTc3twDenynHhaXbPsP\nzGeKiAsGIFKRUxc5I5xnQBk6Fy6LZvGwfif07AcECER+nzffSOMPYFVbhlRuBwOg\n/tJcut77KOEpJIQSwqzT0FOw4oFtkvJt/nhpQMkXwOjEuiMOVpPoXUIpWjnbvNmy\nIPXdnKN4QqHi0fAE+FvKGbNmr18vqApT/D4Yen6W1ZWCRdUR1jjl8LNFBkPH/Tad\nkOj+UyRRJjRRqY5mXCI72Bmhwmi/YdS4gt9K73okOZ3atM+9Kfj3azZm8pP7fRkK\n/lwRP8RZFSSpz4w9JtzYmR7P8qTaxwMuq8VrxtFmf1IBChFpyNHUDtmC9MzLBRE7\n+fnpr1bARR3OwO83/xtT+vKNE+2SBvsf7zeFRXa+p5dGaih90rQOwL8EsUItiG61\nm4y9n3Q7BM7XwrZ7sGe3Hey5SWveOEgemfP4ANJBiMQpU69LKM9dGW1FcEX4FlwW\nZx/135nzMXE2cF+y+q/yY2FlacXPqJXMY32mIc+rHMzvFY/ZDzjRY/7Gg2ekjXuN\n1o7Ag7a+5k+r+XkWBNKIHp8CAwEAAQ==\n-----END PUBLIC KEY-----\n"},"url":"https://demo.gancio.org"}

View file

@ -0,0 +1,80 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"addressRegion": "sc:addressRegion",
"timezone": {"@id": "mz:timezone", "@type": "sc:Text"},
"isOnline": {"@id": "mz:isOnline", "@type": "sc:Boolean"},
"pt": "https://joinpeertube.org/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inLanguage": "sc:inLanguage",
"address": {"@id": "sc:address", "@type": "sc:PostalAddress"},
"discoverable": "toot:discoverable",
"repliesModerationOption": {
"@id": "mz:repliesModerationOption",
"@type": "mz:repliesModerationOptionType"
},
"sc": "http://schema.org#",
"mz": "https://joinmobilizon.org/ns#",
"category": "sc:category",
"joinModeType": {"@id": "mz:joinModeType", "@type": "rdfs:Class"},
"Hashtag": "as:Hashtag",
"propertyID": "sc:propertyID",
"PostalAddress": "sc:PostalAddress",
"discussions": {"@id": "mz:discussions", "@type": "@id"},
"remainingAttendeeCapacity": "sc:remainingAttendeeCapacity",
"streetAddress": "sc:streetAddress",
"anonymousParticipationEnabled": {
"@id": "mz:anonymousParticipationEnabled",
"@type": "sc:Boolean"
},
"addressLocality": "sc:addressLocality",
"joinMode": {"@id": "mz:joinMode", "@type": "mz:joinModeType"},
"location": {"@id": "sc:location", "@type": "sc:Place"},
"toot": "http://joinmastodon.org/ns#",
"participantCount": {
"@id": "mz:participantCount",
"@type": "sc:Integer"
},
"uuid": "sc:identifier",
"maximumAttendeeCapacity": "sc:maximumAttendeeCapacity",
"participationMessage": {
"@id": "mz:participationMessage",
"@type": "sc:Text"
},
"openness": {"@id": "mz:openness", "@type": "@id"},
"members": {"@id": "mz:members", "@type": "@id"},
"events": {"@id": "mz:events", "@type": "@id"},
"resources": {"@id": "mz:resources", "@type": "@id"},
"addressCountry": "sc:addressCountry",
"posts": {"@id": "mz:posts", "@type": "@id"},
"commentsEnabled": {
"@id": "pt:commentsEnabled",
"@type": "sc:Boolean"
},
"value": "sc:value",
"PropertyValue": "sc:PropertyValue",
"repliesModerationOptionType": {
"@id": "mz:repliesModerationOptionType",
"@type": "rdfs:Class"
},
"todos": {"@id": "mz:todos", "@type": "@id"},
"ical": "http://www.w3.org/2002/12/cal/ical#",
"postalCode": "sc:postalCode",
"memberCount": {"@id": "mz:memberCount", "@type": "sc:Integer"},
"@language": "und"
}
],
"actor": "https://mobilizon.org/@tcit",
"id": "https://mobilizon.mkljczk.pl/accept/join/fef2a925-cce5-4b8e-b12f-20afe01e5a0f",
"object": {
"actor": "https://pleroma.mkljczk.pl/users/mkljczk",
"id": "https://pleroma.mkljczk.pl/activities/7d1f3986-8b2c-48c2-b89e-d27ba8459777",
"object": "https://mobilizon.mkljczk.pl/events/d9d08e46-81af-4ee9-91e5-1298f49beea9",
"participationMessage": null,
"published": "2022-10-07T18:53:53Z",
"type": "Join"
},
"type": "Accept"
}

View file

@ -0,0 +1,74 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"addressRegion": "sc:addressRegion",
"timezone": { "@id": "mz:timezone", "@type": "sc:Text" },
"isOnline": { "@id": "mz:isOnline", "@type": "sc:Boolean" },
"pt": "https://joinpeertube.org/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inLanguage": "sc:inLanguage",
"address": { "@id": "sc:address", "@type": "sc:PostalAddress" },
"discoverable": "toot:discoverable",
"repliesModerationOption": {
"@id": "mz:repliesModerationOption",
"@type": "mz:repliesModerationOptionType"
},
"sc": "http://schema.org#",
"mz": "https://joinmobilizon.org/ns#",
"category": "sc:category",
"joinModeType": { "@id": "mz:joinModeType", "@type": "rdfs:Class" },
"Hashtag": "as:Hashtag",
"propertyID": "sc:propertyID",
"PostalAddress": "sc:PostalAddress",
"discussions": { "@id": "mz:discussions", "@type": "@id" },
"remainingAttendeeCapacity": "sc:remainingAttendeeCapacity",
"streetAddress": "sc:streetAddress",
"anonymousParticipationEnabled": {
"@id": "mz:anonymousParticipationEnabled",
"@type": "sc:Boolean"
},
"addressLocality": "sc:addressLocality",
"joinMode": { "@id": "mz:joinMode", "@type": "mz:joinModeType" },
"location": { "@id": "sc:location", "@type": "sc:Place" },
"toot": "http://joinmastodon.org/ns#",
"participantCount": {
"@id": "mz:participantCount",
"@type": "sc:Integer"
},
"uuid": "sc:identifier",
"maximumAttendeeCapacity": "sc:maximumAttendeeCapacity",
"participationMessage": {
"@id": "mz:participationMessage",
"@type": "sc:Text"
},
"openness": { "@id": "mz:openness", "@type": "@id" },
"members": { "@id": "mz:members", "@type": "@id" },
"events": { "@id": "mz:events", "@type": "@id" },
"resources": { "@id": "mz:resources", "@type": "@id" },
"addressCountry": "sc:addressCountry",
"posts": { "@id": "mz:posts", "@type": "@id" },
"commentsEnabled": {
"@id": "pt:commentsEnabled",
"@type": "sc:Boolean"
},
"value": "sc:value",
"PropertyValue": "sc:PropertyValue",
"repliesModerationOptionType": {
"@id": "mz:repliesModerationOptionType",
"@type": "rdfs:Class"
},
"todos": { "@id": "mz:todos", "@type": "@id" },
"ical": "http://www.w3.org/2002/12/cal/ical#",
"postalCode": "sc:postalCode",
"memberCount": { "@id": "mz:memberCount", "@type": "sc:Integer" },
"@language": "und"
}
],
"actor": "https://mobilizon.org/@tcit",
"id": "https://mobilizon.mkljczk.pl/join/event/d1828aac-b57f-43c9-ac87-17388fab2bc8",
"object": "https://pleroma.mkljczk.pl/objects/b800bdb9-2cbb-40b6-9b29-196cfbd42398",
"participationMessage": null,
"type": "Join"
}

View file

@ -0,0 +1,78 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"addressRegion": "sc:addressRegion",
"timezone": { "@id": "mz:timezone", "@type": "sc:Text" },
"isOnline": { "@id": "mz:isOnline", "@type": "sc:Boolean" },
"pt": "https://joinpeertube.org/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inLanguage": "sc:inLanguage",
"address": { "@id": "sc:address", "@type": "sc:PostalAddress" },
"status": { "@id": "ical:status", "@type": "ical:status" },
"discoverable": "toot:discoverable",
"repliesModerationOption": {
"@id": "mz:repliesModerationOption",
"@type": "mz:repliesModerationOptionType"
},
"sc": "http://schema.org#",
"mz": "https://joinmobilizon.org/ns#",
"category": "sc:category",
"joinModeType": { "@id": "mz:joinModeType", "@type": "rdfs:Class" },
"Hashtag": "as:Hashtag",
"propertyID": "sc:propertyID",
"PostalAddress": "sc:PostalAddress",
"discussions": { "@id": "mz:discussions", "@type": "@id" },
"remainingAttendeeCapacity": "sc:remainingAttendeeCapacity",
"streetAddress": "sc:streetAddress",
"anonymousParticipationEnabled": {
"@id": "mz:anonymousParticipationEnabled",
"@type": "sc:Boolean"
},
"externalParticipationUrl": {
"@id": "mz:externalParticipationUrl",
"@type": "sc:URL"
},
"addressLocality": "sc:addressLocality",
"joinMode": { "@id": "mz:joinMode", "@type": "mz:joinModeType" },
"location": { "@id": "sc:location", "@type": "sc:Place" },
"toot": "http://joinmastodon.org/ns#",
"participantCount": {
"@id": "mz:participantCount",
"@type": "sc:Integer"
},
"uuid": "sc:identifier",
"maximumAttendeeCapacity": "sc:maximumAttendeeCapacity",
"participationMessage": {
"@id": "mz:participationMessage",
"@type": "sc:Text"
},
"openness": { "@id": "mz:openness", "@type": "@id" },
"members": { "@id": "mz:members", "@type": "@id" },
"events": { "@id": "mz:events", "@type": "@id" },
"resources": { "@id": "mz:resources", "@type": "@id" },
"addressCountry": "sc:addressCountry",
"posts": { "@id": "mz:posts", "@type": "@id" },
"commentsEnabled": {
"@id": "pt:commentsEnabled",
"@type": "sc:Boolean"
},
"value": "sc:value",
"PropertyValue": "sc:PropertyValue",
"repliesModerationOptionType": {
"@id": "mz:repliesModerationOptionType",
"@type": "rdfs:Class"
},
"todos": { "@id": "mz:todos", "@type": "@id" },
"ical": "http://www.w3.org/2002/12/cal/ical#",
"postalCode": "sc:postalCode",
"memberCount": { "@id": "mz:memberCount", "@type": "sc:Integer" },
"@language": "und"
}
],
"actor": "https://mobilizon.org/@tcit",
"id": "https://mobilizon.mkljczk.pl/leave/event/d1828aac-b57f-43c9-ac87-17388fab2bc8",
"object": "https://pleroma.mkljczk.pl/objects/b800bdb9-2cbb-40b6-9b29-196cfbd42398",
"type": "Leave"
}

View file

@ -0,0 +1 @@
{"type":"FeatureCollection","geocoding":{"version":"0.1.0","attribution":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","licence":"ODbL","query":"Benis"},"features":[{"type":"Feature","properties":{"geocoding":{"place_id":45392146,"osm_type":"node","osm_id":3726208425,"osm_key":"place","osm_value":"village","type":"city","label":"Benis, بخش مرکزی, Shabestar County, East Azerbaijan Province, Iran","name":"Benis","country":"Iran","state":"East Azerbaijan Province","county":"Shabestar County","city":"بخش مرکزی","admin":{"level4":"East Azerbaijan Province","level5":"Shabestar County","level6":"بخش مرکزی"}}},"geometry":{"type":"Point","coordinates":[45.7285348,38.212263]}},{"type":"Feature","properties":{"geocoding":{"place_id":298521494,"osm_type":"relation","osm_id":8841590,"osm_key":"boundary","osm_value":"administrative","type":"district","label":"Bénis, Castelsarrasin, Tarn-et-Garonne, Occitania, Metropolitan France, 82100, France","name":"Bénis","country":"France","postcode":"82100","state":"Occitania","county":"Tarn-et-Garonne","city":"Castelsarrasin","admin":{"level3":"Metropolitan France","level4":"Occitania","level6":"Tarn-et-Garonne","level7":"Castelsarrasin","level8":"Castelsarrasin","level10":"Bénis"}}},"geometry":{"type":"Point","coordinates":[1.1304739,44.0099561]}},{"type":"Feature","properties":{"geocoding":{"place_id":46542003,"osm_type":"node","osm_id":3839840986,"osm_key":"place","osm_value":"locality","type":"locality","label":"Beniš, Jablonov nad Turňou, District of Rožňava, Region of Košice, Eastern Slovakia, 049 43, Slovakia","name":"Beniš","country":"Slovakia","postcode":"049 43","state":"Region of Košice","county":"District of Rožňava","city":"Jablonov nad Turňou","district":"Jablonov nad Turňou","admin":{"level3":"Eastern Slovakia","level4":"Region of Košice","level8":"District of Rožňava","level9":"Jablonov nad Turňou","level10":"Jablonov nad Turňou"}}},"geometry":{"type":"Point","coordinates":[20.6856788,48.5855164]}},{"type":"Feature","properties":{"geocoding":{"place_id":49659169,"osm_type":"node","osm_id":4261667102,"osm_key":"shop","osm_value":"supermarket","type":"house","label":"Benis, Carrera 18, Centro, Comuna El Cafetero, Perímetro Urbano Armenia, Armenia, Capital, Quindío, 630004, Colombia","name":"Benis","country":"Colombia","postcode":"630004","state":"Quindío","county":"Capital","city":"Armenia","district":"Comuna El Cafetero","locality":"Centro","street":"Carrera 18","admin":{"level4":"Quindío","level5":"Capital","level6":"Armenia","level7":"Perímetro Urbano Armenia","level8":"Comuna El Cafetero"}}},"geometry":{"type":"Point","coordinates":[-75.6734958,4.5362242]}},{"type":"Feature","properties":{"geocoding":{"place_id":65944064,"osm_type":"node","osm_id":6087846386,"osm_key":"amenity","osm_value":"cafe","type":"house","label":"Bénis, شارع الحرية, الحدائق, معتمدية باب بحر, Tunis, 1017, Tunisia","name":"Bénis","country":"Tunisia","postcode":"1017","state":"Tunis","city":"Tunis","district":"الحدائق","street":"شارع الحرية","admin":{"level4":"Tunis","level5":"معتمدية باب بحر","level6":"الحدائق"}}},"geometry":{"type":"Point","coordinates":[10.1796633,36.8103671]}},{"type":"Feature","properties":{"geocoding":{"place_id":19666304,"osm_type":"node","osm_id":2250812267,"osm_key":"shop","osm_value":"bakery","type":"house","label":"Bénis, Rue du Fqih al Abbas, batiment hay dakhla, Houbous District, Mechouar, Pachalik de Méchouar de Casablanca, Prefecture of Casablanca, Casablanca-Settat, 20504, Morocco","name":"Bénis","country":"Morocco","postcode":"20504","state":"Casablanca-Settat","county":"Pachalik de Méchouar de Casablanca","city":"Mechouar","district":"Houbous District","locality":"batiment hay dakhla","street":"Rue du Fqih al Abbas","admin":{"level4":"Casablanca-Settat","level5":"Prefecture of Casablanca","level6":"Pachalik de Méchouar de Casablanca","level8":"Mechouar"}}},"geometry":{"type":"Point","coordinates":[-7.6054449,33.5779601]}}]}

View file

@ -0,0 +1 @@
{"type":"FeatureCollection","geocoding":{"version":"0.1.0","attribution":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","licence":"ODbL","query":"N3726208425,R3726208425,W3726208425"},"features":[{"type":"Feature","properties":{"geocoding":{"place_id":45392146,"osm_type":"node","osm_id":3726208425,"osm_key":"place","osm_value":"village","type":"city","label":"Benis, بخش مرکزی, Shabestar County, East Azerbaijan Province, Iran","name":"Benis","country":"Iran","state":"East Azerbaijan Province","county":"Shabestar County","city":"بخش مرکزی","admin":{"level4":"East Azerbaijan Province","level5":"Shabestar County","level6":"بخش مرکزی"}}},"geometry":{"type":"Point","coordinates":[45.7285348,38.212263]}}]}

View file

@ -0,0 +1 @@
{"@context":["https:\/\/schema.org\/","https:\/\/www.w3.org\/ns\/activitystreams",{"pt":"https:\/\/joinpeertube.org\/ns#","mz":"https:\/\/joinmobilizon.org\/ns#","status":"http:\/\/www.w3.org\/2002\/12\/cal\/ical#status","commentsEnabled":"pt:commentsEnabled","isOnline":"mz:isOnline","timezone":"mz:timezone","participantCount":"mz:participantCount","anonymousParticipationEnabled":"mz:anonymousParticipationEnabled","joinMode":{"@id":"mz:joinMode","@type":"mz:joinModeType"},"externalParticipationUrl":{"@id":"mz:externalParticipationUrl","@type":"schema:URL"},"repliesModerationOption":{"@id":"mz:repliesModerationOption","@type":"@vocab"},"contacts":{"@id":"mz:contacts","@type":"@id"}}],"id":"https:\/\/wp-test.event-federation.eu\/?p=227","type":"Event","attachment":[{"type":"Image","url":"https:\/\/wp-test.event-federation.eu\/wp-content\/uploads\/2024\/10\/christmas-trees-seamless-pattern-1698986532LKN-1024x1024.jpg","mediaType":"image\/jpeg","name":"Alt!"}],"attributedTo":"https:\/\/wp-test.event-federation.eu\/@test","content":"\u003Cp\u003EFantastic description!\u003C\/p\u003E","contentMap":{"en":"\u003Cp\u003EFantastic description!\u003C\/p\u003E"},"name":"Test Event","nameMap":{"en":"Test Event"},"endTime":"2025-02-27T17:00:00+01:00","icon":{"type":"Image","url":"https:\/\/wp-test.event-federation.eu\/wp-content\/uploads\/2024\/10\/christmas-trees-seamless-pattern-1698986532LKN-150x150.jpg","mediaType":"image\/jpeg","name":"Alt!"},"image":{"type":"Image","url":"https:\/\/wp-test.event-federation.eu\/wp-content\/uploads\/2024\/10\/christmas-trees-seamless-pattern-1698986532LKN-1024x1024.jpg","mediaType":"image\/jpeg","name":"Alt!"},"location":{"id":"https:\/\/wp-test.event-federation.eu\/venue\/morelli\/","type":"Place","attributedTo":"https:\/\/wp-test.event-federation.eu\/@test","name":"Morelli","nameMap":{"en":"Morelli"},"published":"2024-09-29T07:46:54Z","updated":"2024-10-22T15:01:56Z","url":"https:\/\/wp-test.event-federation.eu\/venue\/morelli\/","likes":{"id":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/posts\/30\/likes","type":"Collection","totalItems":0},"shares":{"id":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/posts\/30\/shares","type":"Collection","totalItems":0},"sensitive":false,"address":{"type":"PostalAddress","addressCountry":"Austria","addressLocality":"Graz","postalCode":"8010","streetAddress":"Morelli"}},"published":"2025-02-05T17:39:51Z","startTime":"2025-02-27T08:00:00+01:00","summary":"\u003Cp\u003E\ud83d\uddd3\ufe0f Start: February 27, 2025 8:00 am\u003Cbr \/\u003E\u23f3 End: February 27, 2025 5:00 pm\u003Cbr \/\u003E\ud83d\udccd Location: Morelli, Morelli, 8010, Graz, Austria\u003C\/p\u003E\u003Cp\u003EFantastic description!\u003C\/p\u003E","summaryMap":{"en":"\u003Cp\u003E\ud83d\uddd3\ufe0f Start: February 27, 2025 8:00 am\u003Cbr \/\u003E\u23f3 End: February 27, 2025 5:00 pm\u003Cbr \/\u003E\ud83d\udccd Location: Morelli, Morelli, 8010, Graz, Austria\u003C\/p\u003E\u003Cp\u003EFantastic description!\u003C\/p\u003E"},"tag":[{"type":"Hashtag","href":"https:\/\/wp-test.event-federation.eu\/tag\/musik\/","name":"#Musik"}],"updated":"2025-02-05T17:40:20Z","url":"https:\/\/wp-test.event-federation.eu\/event\/test-event\/","to":["https:\/\/www.w3.org\/ns\/activitystreams#Public","https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/actors\/0\/followers"],"mediaType":"text\/html","replies":{"id":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/posts\/227\/replies","type":"Collection","first":{"id":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/posts\/227\/replies?page=1","type":"CollectionPage","partOf":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/posts\/227\/replies","items":[]}},"likes":{"id":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/posts\/227\/likes","type":"Collection","totalItems":0},"shares":{"id":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/posts\/227\/shares","type":"Collection","totalItems":0},"sensitive":false,"commentsEnabled":true,"timezone":"Europe\/Vienna","repliesModerationOption":"allow_all","category":"LGBTQ","isOnline":false,"status":"CONFIRMED","externalParticipationUrl":"https:\/\/wp-test.event-federation.eu\/event\/test-event\/","joinMode":"external"}

View file

@ -0,0 +1 @@
{"@context":["https:\/\/www.w3.org\/ns\/activitystreams","https:\/\/w3id.org\/security\/v1","https:\/\/purl.archive.org\/socialweb\/webfinger",{"schema":"http:\/\/schema.org#","toot":"http:\/\/joinmastodon.org\/ns#","lemmy":"https:\/\/join-lemmy.org\/ns#","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","PropertyValue":"schema:PropertyValue","value":"schema:value","Hashtag":"as:Hashtag","featured":{"@id":"toot:featured","@type":"@id"},"featuredTags":{"@id":"toot:featuredTags","@type":"@id"},"moderators":{"@id":"lemmy:moderators","@type":"@id"},"attributionDomains":{"@id":"toot:attributionDomains","@type":"@id"},"postingRestrictedToMods":"lemmy:postingRestrictedToMods","discoverable":"toot:discoverable","indexable":"toot:indexable"}],"id":"https:\/\/wp-test.event-federation.eu\/@test","type":"Person","attachment":[{"type":"PropertyValue","name":"Blog","value":"<p><a href=\"https:\/\/wp-test.event-federation.eu\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https:\/\/<\/span><span class=\"\">wp-test.event-federation.eu\/<\/span><span class=\"invisible\"><\/span><\/a><\/p>"},{"type":"Link","name":"Blog","href":"https:\/\/wp-test.event-federation.eu\/","rel":["nofollow","noopener","noreferrer","me"]}],"name":"WP Test","icon":{"type":"Image","url":"https:\/\/wp-test.event-federation.eu\/wp-content\/plugins\/activitypub\/assets\/img\/wp-logo.png"},"image":{"type":"Image","url":"https:\/\/wp-test.event-federation.eu\/wp-content\/uploads\/2025\/02\/cropped-1a14b2a8eebccd6995543a3925034f4b.jpg"},"published":"2024-04-15T10:25:30Z","summary":"","tag":[{"type":"Hashtag","href":"https:\/\/wp-test.event-federation.eu\/tag\/musik\/","name":"#Musik"},{"type":"Hashtag","href":"https:\/\/wp-test.event-federation.eu\/tag\/gathering\/","name":"#Gathering"},{"type":"Hashtag","href":"https:\/\/wp-test.event-federation.eu\/tag\/forest\/","name":"#forest"},{"type":"Hashtag","href":"https:\/\/wp-test.event-federation.eu\/tag\/party\/","name":"#party"},{"type":"Hashtag","href":"https:\/\/wp-test.event-federation.eu\/tag\/testing\/","name":"#testing"}],"url":"https:\/\/wp-test.event-federation.eu\/@test","inbox":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/actors\/0\/inbox","outbox":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/actors\/0\/outbox","following":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/actors\/0\/following","followers":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/actors\/0\/followers","streams":[],"preferredUsername":"test","publicKey":{"id":"https:\/\/wp-test.event-federation.eu\/@test#main-key","owner":"https:\/\/wp-test.event-federation.eu\/@test","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyClGfGBcTPXYn2J6lASE\nYVGR0JWS7gefeZ47QmuMzdrdgvfbackWGv5jnAdD\/AcXejYM\/NBioM9aoiRsGkSk\nvpan\/yrFeHdfGY\/L8NAMHwepjnQlFcfEPg0izYduJYhthDyVJPAorLXO16BTf2t7\nN64qOnNQKuMiIgBkk+R+3dhgrYK01V9F1r7IqLpu9gVmY3YuRDNHUH6SWfeE798f\niZ3BhqOjeegslhrMr7vR+ptbvbwFeIbmrNOKt4dkLmYcMuV7CaeBndJfrRpPpABu\nJ4Nzl7sJ6PvqUal0FlkpjTQBwg1Y4ogVx6G1UqXD5bNgGS4\/mndWnfiY3bHgKAOU\nFQIDAQAB\n-----END PUBLIC KEY-----\n"},"manuallyApprovesFollowers":false,"attributionDomains":["wp-test.event-federation.eu"],"featured":"https:\/\/wp-test.event-federation.eu\/wp-json\/activitypub\/1.0\/actors\/0\/collections\/featured","indexable":true,"webfinger":"test@wp-test.event-federation.eu","discoverable":true}

View file

@ -173,6 +173,128 @@ defmodule Pleroma.NotificationTest do
assert [%{type: "update"}] = Notification.for_user(repeated_user)
assert [%{type: "mention"}] = Notification.for_user(other_user)
end
test "creates notification for event join request" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(user, %{
name: "test event",
status: "",
join_mode: "restricted",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(other_user, activity.id)
[notification] = user_notifications = Notification.for_user(user)
assert length(user_notifications) == 1
assert notification.type == "pleroma:participation_request"
end
test "creates notification for event join approval" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(user, %{
name: "test event",
status: "",
join_mode: "restricted",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(other_user, activity.id)
CommonAPI.accept_join_request(user, other_user, activity.data["object"])
[notification] = user_notifications = Notification.for_user(other_user)
assert length(user_notifications) == 1
assert notification.type == "pleroma:participation_accepted"
end
test "doesn't create notification for events without participation approval" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(user, %{
name: "test event",
status: "",
join_mode: "free",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(other_user, activity.id)
user_notifications = Notification.for_user(user)
assert length(user_notifications) == 0
user_notifications = Notification.for_user(other_user)
assert length(user_notifications) == 0
end
end
test "creates notifications for edited events for participants" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(user, %{
name: "test event",
status: "test evnet",
join_mode: "free",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(other_user, activity.id)
{:ok, _edit_activity} =
CommonAPI.update_event(user, activity, %{
name: "test event",
status: "test event",
join_mode: "free",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
Pleroma.Tests.ObanHelpers.perform_all()
[notification] = user_notifications = Notification.for_user(other_user)
assert length(user_notifications) == 1
assert notification.type == "pleroma:event_update"
end
test "doesn't create multiple edit notifications for events" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(user, %{
name: "test event",
status: "test evnet",
join_mode: "free",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(other_user, activity.id)
CommonAPI.repeat(activity.id, other_user)
{:ok, _edit_activity} =
CommonAPI.update_event(user, activity, %{
name: "test event",
status: "test event",
join_mode: "free",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
Pleroma.Tests.ObanHelpers.perform_all()
user_notifications = Notification.for_user(other_user)
assert length(user_notifications) == 1
end
test "create_poll_notifications/1" do

View file

@ -491,6 +491,33 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
end
end
describe "Event objects" do
setup do
user = insert(:user)
event = build(:event, user: user)
event_activity = build(:event_activity, event: event)
activity_data = Map.put(event_activity.data, "object", event.data["id"])
meta = [object_data: event.data, local: false]
{:ok, activity, meta} = ActivityPub.persist(activity_data, meta)
%{activity: activity, meta: meta}
end
test "enqueues event reminder notification worker", %{activity: activity, meta: meta} do
{:ok, activity, meta} = SideEffects.handle(activity, meta)
assert_enqueued(
worker: Pleroma.Workers.EventReminderWorker,
args: %{op: "event_reminder", activity_id: activity.id},
scheduled_at:
DateTime.from_iso8601(meta[:object_data]["startTime"])
|> elem(1)
|> DateTime.add(60 * 60 * -2, :second)
)
end
end
describe "delete users with confirmation pending" do
setup do
user = insert(:user, is_confirmed: false)

View file

@ -5,6 +5,8 @@
defmodule Pleroma.Web.ActivityPub.Transmogrifier.AcceptHandlingTest do
use Pleroma.DataCase, async: true
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
@ -88,4 +90,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AcceptHandlingTest do
refute User.following?(follower, followed) == true
end
test "it works for incoming Mobilizon join accepts" do
event_author = insert(:user)
participant = insert(:user)
event = insert(:event, %{user: event_author, data: %{"joinMode" => "restricted"}})
event_activity = insert(:event_activity, event: event)
{:ok, join_activity} = CommonAPI.join(participant, event_activity.id)
accept_data =
File.read!("test/fixtures/tesla_mock/mobilizon-event-join-accept.json")
|> Jason.decode!()
|> Map.put("actor", event_author.ap_id)
|> Map.put("object", join_activity.data["id"])
{:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(accept_data)
event = Object.get_by_id(event.id)
assert event.data["participations"] == [participant.ap_id]
join_activity = Repo.get(Activity, join_activity.id)
assert join_activity.data["state"] == "accept"
end
end

View file

@ -38,5 +38,57 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EventHandlingTest do
assert object.data["published"] == "2019-12-17T11:33:56Z"
assert object.data["name"] == "Mobilizon Launching Party"
assert object.data["startTime"] == "2019-12-18T13:00:00Z"
assert object.data["endTime"] == "2019-12-18T14:00:00Z"
assert object.data["location"] == %{
"address" => %{
"addressCountry" => "France",
"addressLocality" => "Nantes",
"addressRegion" => "Pays de la Loire",
"type" => "PostalAddress"
},
"name" => "Cour du Château des Ducs de Bretagne",
"type" => "Place"
}
end
test "Gancio Event object" do
Tesla.Mock.mock(fn
%{url: "https://demo.gancio.org/federation/m/1"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/gancio-event.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
%{url: "https://demo.gancio.org/federation/u/customized"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/gancio-user.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
end)
assert {:ok, object} = Fetcher.fetch_object_from_id("https://demo.gancio.org/federation/m/1")
assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
# assert object.data["cc"] == ["https://demo.gancio.org/federation/u/customized/followers"]
assert object.data["url"] == "https://demo.gancio.org/event/demo-event"
assert object.data["published"] == "2021-07-01T22:33:36.543Z"
assert object.data["name"] == "Demo event"
assert object.data["startTime"] == "2021-07-14T15:30:57.000Z"
assert object.data["endTime"] == "2021-07-14T16:30:57.000Z"
assert object.data["location"] == %{
"address" => %{
"streetAddress" => "Piazza del Colosseo, Rome",
"type" => "PostalAddress"
},
"name" => "Colosseo",
"type" => "Place"
}
end
end

View file

@ -0,0 +1,76 @@
# 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.Transmogrifier.JoinHandlingTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
import Ecto.Query
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "handle_incoming" do
test "it works for incoming Mobilizon joins" do
user = insert(:user)
event = insert(:event)
join_data =
File.read!("test/fixtures/tesla_mock/mobilizon-event-join.json")
|> Jason.decode!()
|> Map.put("actor", user.ap_id)
|> Map.put("object", event.data["id"])
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(join_data)
event = Object.get_by_id(event.id)
assert event.data["participations"] == [join_data["actor"]]
activity = Repo.get(Activity, activity.id)
assert activity.data["state"] == "accept"
end
test "with restricted events, it does create a Join, but not an Accept" do
[participant, event_author] = insert_pair(:user)
event = insert(:event, %{user: event_author, data: %{"joinMode" => "restricted"}})
join_data =
File.read!("test/fixtures/tesla_mock/mobilizon-event-join.json")
|> Jason.decode!()
|> Map.put("actor", participant.ap_id)
|> Map.put("object", event.data["id"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(join_data)
event = Object.get_by_id(event.id)
assert event.data["participations"] == nil
assert data["state"] == "pending"
accepts =
from(
a in Activity,
where: fragment("?->>'type' = ?", a.data, "Accept")
)
|> Repo.all()
assert Enum.empty?(accepts)
[notification] = Notification.for_user(event_author)
assert notification.type == "pleroma:participation_request"
end
end
end

View file

@ -0,0 +1,51 @@
# 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.Transmogrifier.LeaveHandlingTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "handle_incoming" do
test "it works for incoming Mobilizon leaves" do
user = insert(:user)
event = insert(:event)
{:ok, join_activity, _} = Builder.join(user, event)
{:ok, join_activity, _} = Pipeline.common_pipeline(join_activity, local: true)
{:ok, _, _} = SideEffects.handle(join_activity, local: true)
event = Object.get_by_id(event.id)
assert length(event.data["participations"]) === 1
leave_data =
File.read!("test/fixtures/tesla_mock/mobilizon-event-leave.json")
|> Jason.decode!()
|> Map.put("actor", user.ap_id)
|> Map.put("object", event.data["id"])
{:ok, %Activity{local: false} = _activity} = Transmogrifier.handle_incoming(leave_data)
event = Object.get_by_id(event.id)
assert length(event.data["participations"]) === 0
refute Repo.get(Activity, join_activity.id)
end
end
end

View file

@ -670,4 +670,51 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
)
end
end
describe "add_participation_to_object/2" do
test "add actor to participations" do
user = insert(:user)
user2 = insert(:user)
object = insert(:event)
assert {:ok, updated_object} =
Utils.add_participation_to_object(
%Activity{data: %{"actor" => user.ap_id}},
object
)
assert updated_object.data["participations"] == [user.ap_id]
assert updated_object.data["participation_count"] == 1
assert {:ok, updated_object2} =
Utils.add_participation_to_object(
%Activity{data: %{"actor" => user2.ap_id}},
updated_object
)
assert updated_object2.data["participations"] == [user2.ap_id, user.ap_id]
assert updated_object2.data["participation_count"] == 2
end
end
describe "remove_participation_from_object/2" do
test "removes ap_id from participations" do
user = insert(:user)
user2 = insert(:user)
object =
insert(:event,
data: %{"participations" => [user.ap_id, user2.ap_id], "participation_count" => 2}
)
assert {:ok, updated_object} =
Utils.remove_participation_from_object(
%Activity{data: %{"actor" => user.ap_id}},
object
)
assert updated_object.data["participations"] == [user2.ap_id]
assert updated_object.data["participation_count"] == 1
end
end
end

View file

@ -2464,6 +2464,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(:forbidden)
end
test "it refuses to update an event", %{conn: conn, user: user} do
{:ok, activity} =
CommonAPI.event(user, %{
name: "I'm not a regular status",
status: "",
join_mode: "free",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited"
})
|> json_response_and_validate_schema(:unprocessable_entity)
end
test "it returns 404 if the user cannot see the post", %{conn: conn} do
another_user = insert(:user)

View file

@ -343,7 +343,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
pinned_at: nil,
quotes_count: 0,
bookmark_folder: nil,
list_id: nil
list_id: nil,
event: nil
}
}
@ -742,7 +743,28 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
assert represented[:content] ==
"<p><a href=\"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39\">Mobilizon Launching Party</a></p><p>Mobilizon is now federated! 🎉</p><p></p><p>You can view this event from other instances if they are subscribed to mobilizon.org, and soon directly from Mastodon and Pleroma. It is possible that you may see some comments from other instances, including Mastodon ones, just below.</p><p></p><p>With a Mobilizon account on an instance, you may <strong>participate</strong> at events from other instances and <strong>add comments</strong> on events.</p><p></p><p>Of course, it&#39;s still <u>a work in progress</u>: if reports made from an instance on events and comments can be federated, you can&#39;t block people right now, and moderators actions are rather limited, but this <strong>will definitely get fixed over time</strong> until first stable version next year.</p><p></p><p>Anyway, if you want to come up with some feedback, head over to our forum or - if you feel you have technical skills and are familiar with it - on our Gitlab repository.</p><p></p><p>Also, to people that want to set Mobilizon themselves even though we really don&#39;t advise to do that for now, we have a little documentation but it&#39;s quite the early days and you&#39;ll probably need some help. No worries, you can chat with us on our Forum or though our Matrix channel.</p><p></p><p>Check our website for more informations and follow us on Twitter or Mastodon.</p>"
"<p>Mobilizon is now federated! 🎉</p><p></p><p>You can view this event from other instances if they are subscribed to mobilizon.org, and soon directly from Mastodon and Pleroma. It is possible that you may see some comments from other instances, including Mastodon ones, just below.</p><p></p><p>With a Mobilizon account on an instance, you may <strong>participate</strong> at events from other instances and <strong>add comments</strong> on events.</p><p></p><p>Of course, it&#39;s still <u>a work in progress</u>: if reports made from an instance on events and comments can be federated, you can&#39;t block people right now, and moderators actions are rather limited, but this <strong>will definitely get fixed over time</strong> until first stable version next year.</p><p></p><p>Anyway, if you want to come up with some feedback, head over to our forum or - if you feel you have technical skills and are familiar with it - on our Gitlab repository.</p><p></p><p>Also, to people that want to set Mobilizon themselves even though we really don&#39;t advise to do that for now, we have a little documentation but it&#39;s quite the early days and you&#39;ll probably need some help. No worries, you can chat with us on our Forum or though our Matrix channel.</p><p></p><p>Check our website for more informations and follow us on Twitter or Mastodon.</p>"
assert represented.pleroma.event == %{
name: "Mobilizon Launching Party",
start_time: "2019-12-18T13:00:00Z",
end_time: "2019-12-18T14:00:00Z",
join_mode: "free",
participants_count: 0,
location: %{
country: "France",
latitude: nil,
locality: "Nantes",
longitude: nil,
name: "Cour du Château des Ducs de Bretagne",
postal_code: nil,
region: "Pays de la Loire",
street: nil,
url: nil
},
join_state: nil,
participation_request_count: nil
}
end
describe "build_tags/1" do

View file

@ -0,0 +1,412 @@
# 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.EventControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
describe "POST /api/v1/pleroma/events" do
setup do
user = insert(:user)
%{user: user, conn: conn} = oauth_access(["write"], user: user)
[user: user, conn: conn]
end
test "creates an event", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/pleroma/events", %{
"name" => "Event name",
"start_time" => "2023-01-01T01:00:00.000Z",
"end_time" => "2023-01-01T04:00:00.000Z",
"join_mode" => "free"
})
assert %{
"pleroma" => %{
"event" => %{
"name" => "Event name",
"join_mode" => "free"
}
}
} = json_response_and_validate_schema(conn, 200)
end
test "can't create event that ends before its start", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/pleroma/events", %{
"name" => "Event name",
"start_time" => "2023-01-01T04:00:00.000Z",
"end_time" => "2022-12-31T04:00:00.000Z",
"join_mode" => "free"
})
assert json_response_and_validate_schema(conn, 422) == %{
"error" => "Event can't end before its start"
}
end
test "assigns location from location id", %{conn: conn} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/pleroma/events", %{
"name" => "Event name",
"start_time" => "2023-01-01T01:00:00.000Z",
"end_time" => "2023-01-01T04:00:00.000Z",
"join_mode" => "free",
"location_id" => "3726208425"
})
assert %{
"pleroma" => %{
"event" => %{
"location" => %{
"name" => "Benis",
"longitude" => 45.7285348,
"latitude" => 38.212263,
"street" => " ",
"locality" => "بخش مرکزی",
"region" => "East Azerbaijan Province",
"country" => "Iran"
}
}
}
} = json_response_and_validate_schema(conn, 200)
end
end
test "GET /api/v1/pleroma/events/:id/participations" do
%{conn: conn} = oauth_access(["read"])
user_one = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
{:ok, activity} =
CommonAPI.event(user_one, %{
name: "test event",
status: "",
join_mode: "free",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(user_two, activity.id)
CommonAPI.join(user_three, activity.id)
conn =
conn
|> get("/api/v1/pleroma/events/#{activity.id}/participations")
assert response = json_response_and_validate_schema(conn, 200)
assert length(response) == 3
end
describe "GET /api/v1/pleroma/events/:id/participation_requests" do
setup do
user = insert(:user)
%{user: user, conn: conn} = oauth_access(["read"], user: user)
[user: user, conn: conn]
end
test "show participation requests", %{conn: conn, user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(user, %{
name: "test event",
status: "",
join_mode: "restricted",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(other_user, activity.id, %{
participation_message: "I'm interested in this event"
})
conn =
conn
|> get("/api/v1/pleroma/events/#{activity.id}/participation_requests")
assert [
%{
"participation_message" => "I'm interested in this event"
}
] = response = json_response_and_validate_schema(conn, 200)
assert length(response) == 1
end
test "don't display requests if not an author", %{conn: conn} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(other_user, %{
name: "test event",
status: "",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
conn =
conn
|> get("/api/v1/pleroma/events/#{activity.id}/participation_requests")
assert %{"error" => "Can't get participation requests"} =
json_response_and_validate_schema(conn, 403)
end
end
describe "POST /api/v1/pleroma/events/:id/join" do
setup do
user = insert(:user)
%{user: user, conn: conn} = oauth_access(["write"], user: user)
[user: user, conn: conn]
end
test "joins an event", %{conn: conn} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(other_user, %{
name: "test event",
status: "",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
conn =
conn
|> post("/api/v1/pleroma/events/#{activity.id}/join")
assert json_response_and_validate_schema(conn, 200)
assert %{
data: %{
"participation_count" => 2
}
} = Object.get_by_ap_id(activity.data["object"])
end
test "can't join your own event", %{conn: conn, user: user} do
{:ok, activity} =
CommonAPI.event(user, %{
name: "test event",
status: "",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
conn =
conn
|> post("/api/v1/pleroma/events/#{activity.id}/join")
assert json_response_and_validate_schema(conn, :bad_request) == %{
"error" => "Can't join your own event"
}
end
test "can't join externally managed event", %{conn: conn} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, object} =
Pleroma.Object.Fetcher.fetch_object_from_id(
"https://wp-test.event-federation.eu/event/test-event/"
)
activity = Pleroma.Activity.get_create_by_object_ap_id(object.data["id"])
conn =
conn
|> post("/api/v1/pleroma/events/#{activity.id}/join")
assert json_response_and_validate_schema(conn, :bad_request) == %{
"error" => "Joins are managed by external system"
}
end
end
describe "POST /api/v1/pleroma/events/:id/leave" do
setup do
user = insert(:user)
%{user: user, conn: conn} = oauth_access(["write"], user: user)
[user: user, conn: conn]
end
test "leaves an event", %{conn: conn, user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(other_user, %{
name: "test event",
status: "",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(user, activity.id)
conn =
conn
|> post("/api/v1/pleroma/events/#{activity.id}/leave")
assert json_response_and_validate_schema(conn, 200)
assert %{
data: %{
"participation_count" => 1
}
} = Object.get_by_ap_id(activity.data["object"])
end
test "can't leave event you are not participating in", %{conn: conn} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(other_user, %{
name: "test event",
status: "",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
conn =
conn
|> post("/api/v1/pleroma/events/#{activity.id}/leave")
assert json_response_and_validate_schema(conn, :bad_request) == %{
"error" => "Not participating in the event"
}
end
end
describe "POST /api/v1/pleroma/events/:id/participation_requests/:participant_id/authorize" do
setup do
user = insert(:user)
%{user: user, conn: conn} = oauth_access(["write"], user: user)
[user: user, conn: conn]
end
test "accepts a participation request", %{user: user, conn: conn} do
%{ap_id: ap_id} = other_user = insert(:user)
{:ok, activity} =
CommonAPI.event(user, %{
name: "test event",
status: "",
join_mode: "restricted",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(other_user, activity.id)
conn =
conn
|> post(
"/api/v1/pleroma/events/#{activity.id}/participation_requests/#{other_user.id}/authorize"
)
assert json_response_and_validate_schema(conn, 200)
assert %{
data: %{
"participations" => [^ap_id, _],
"participation_count" => 2
}
} = Object.get_by_ap_id(activity.data["object"])
assert %{data: %{"state" => "accept"}} =
Utils.get_existing_join(other_user.ap_id, activity.data["object"])
end
test "it refuses to accept a request when event is not by the user", %{conn: conn} do
[second_user, third_user] = insert_pair(:user)
{:ok, activity} =
CommonAPI.event(second_user, %{
name: "test event",
status: "",
join_mode: "restricted",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(third_user, activity.id)
conn =
conn
|> post(
"/api/v1/pleroma/events/#{activity.id}/participation_requests/#{third_user.id}/authorize"
)
assert json_response_and_validate_schema(conn, :forbidden)
end
end
test "POST /api/v1/pleroma/events/:id/participation_requests/:participant_id/reject" do
[user, other_user] = insert_pair(:user)
%{user: user, conn: conn} = oauth_access(["write"], user: user)
{:ok, activity} =
CommonAPI.event(user, %{
name: "test event",
status: "",
join_mode: "restricted",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
CommonAPI.join(other_user, activity.id)
conn =
conn
|> post(
"/api/v1/pleroma/events/#{activity.id}/participation_requests/#{other_user.id}/reject"
)
assert json_response_and_validate_schema(conn, 200)
assert %{data: %{"state" => "reject"}} =
Utils.get_existing_join(other_user.ap_id, activity.data["object"])
end
test "GET /api/v1/pleroma/events/:id/ics" do
%{conn: conn} = oauth_access(["read"])
user = insert(:user)
{:ok, activity} =
CommonAPI.event(user, %{
name: "test event",
status: "",
join_mode: "free",
start_time: DateTime.from_iso8601("2023-01-01T01:00:00.000Z") |> elem(1)
})
conn =
conn
|> get("/api/v1/pleroma/events/#{activity.id}/ics")
assert conn.status == 200
assert conn.resp_body == """
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
VERSION:2.0
PRODID:-//Elixir ICalendar//Elixir ICalendar//EN
BEGIN:VEVENT
DESCRIPTION:
DTSTART:20230101T010000Z
ORGANIZER:#{Pleroma.HTML.strip_tags(user.name || user.nickname)}
SUMMARY:test event
UID:#{activity.object.id}
URL:#{activity.object.data["id"]}
END:VEVENT
END:VCALENDAR
"""
end
end

View file

@ -0,0 +1,38 @@
# 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.SearchControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
test "GET /api/v1/pleroma/search/location" do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
user = insert(:user)
%{conn: conn} = oauth_access([], user: user)
conn =
conn
|> get("/api/v1/pleroma/search/location?q=Benis")
assert [result | _] = json_response_and_validate_schema(conn, 200)
assert result == %{
"country" => "Iran",
"description" => "Benis",
"geom" => %{"coordinates" => [45.7285348, 38.212263], "srid" => 4326},
"locality" => "بخش مرکزی",
"origin_id" => "3726208425",
"origin_provider" => "nominatim",
"postal_code" => nil,
"region" => "East Azerbaijan Province",
"street" => " ",
"timezone" => nil,
"type" => "city",
"url" => nil
}
end
end

View file

@ -675,4 +675,55 @@ defmodule Pleroma.Factory do
}
|> Map.merge(params)
end
def event_factory(attrs \\ %{}) do
user = attrs[:user] || insert(:user)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"type" => "Event",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"name" => "Event",
"content" => "",
"attachment" => [],
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [user.follower_address],
"context" => Pleroma.Web.ActivityPub.Utils.generate_context_id(),
"startTime" => DateTime.utc_now() |> DateTime.add(14_400) |> DateTime.to_iso8601(),
"endTime" => DateTime.utc_now() |> DateTime.add(18_000) |> DateTime.to_iso8601(),
"joinMode" => "free"
}
%Pleroma.Object{
data: merge_attributes(data, Map.get(attrs, :data, %{}))
}
end
def event_activity_factory(attrs \\ %{}) do
user = attrs[:user] || insert(:user)
event = attrs[:event] || insert(:event, user: user)
data_attrs = attrs[:data_attrs] || %{}
attrs = Map.drop(attrs, [:user, :event, :data_attrs])
data =
%{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => event.data["actor"],
"to" => event.data["to"],
"object" => event.data["id"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => event.data["context"]
}
|> Map.merge(data_attrs)
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
|> Map.merge(attrs)
end
end

View file

@ -1508,6 +1508,34 @@ defmodule HttpRequestMock do
{:ok, %Tesla.Env{status: 200, body: "hello"}}
end
def get(
"https://nominatim.openstreetmap.org/search?format=geocodejson&q=Benis&limit=10&accept-language=en&addressdetails=1&namedetails=1",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/nominatim_search_results.json"),
headers: [{"content-type", "application/json"}]
}}
end
def get(
"https://nominatim.openstreetmap.org/lookup?format=geocodejson&osm_ids=N3726208425,R3726208425,W3726208425&accept-language=en&addressdetails=1&namedetails=1",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/nominatim_single_result.json"),
headers: [{"content-type", "application/json"}]
}}
end
def get("https://friends.grishka.me/posts/54642", _, _, _) do
{:ok,
%Tesla.Env{
@ -1640,6 +1668,41 @@ defmodule HttpRequestMock do
}}
end
def get("https://wp-test.event-federation.eu/event/test-event/", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/wordpress-event.json"),
headers: activitypub_object_headers()
}}
end
def get("https://wp-test.event-federation.eu/@test", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/wordpress-user.json"),
headers: activitypub_object_headers()
}}
end
def get(
"https://wp-test.event-federation.eu/wp-json/activitypub/1.0/actors/0/collections/featured",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "wp-test.event-federation.eu")
|> String.replace("{{nickname}}", "test"),
headers: [{"content-type", "application/activity+json"}]
}}
end
def get(url, query, body, headers) do
{:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}