# Pleroma: A lightweight social networking server # Copyright © 2017-2022 Pleroma Authors # 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.Repo 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, _) do with %Object{data: %{"participations" => participations}} <- Object.normalize(activity, fetch: false) do users = User |> Ecto.Query.where([u], u.ap_id in ^participations) |> Repo.all() |> 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