1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.StatusController do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper,
9 only: [try_render: 3, add_link_headers: 2]
13 alias Pleroma.Activity
14 alias Pleroma.Bookmark
15 alias Pleroma.BookmarkFolder
18 alias Pleroma.ScheduledActivity
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.AccountView
24 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
25 alias Pleroma.Web.OAuth.Token
26 alias Pleroma.Web.Plugs.OAuthScopesPlug
27 alias Pleroma.Web.Plugs.RateLimiter
29 plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
31 plug(:skip_public_check when action in [:index, :show])
33 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
37 %{@unauthenticated_access | scopes: ["read:statuses"]}
49 %{scopes: ["write:statuses"]}
59 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
63 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
68 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
73 %{@unauthenticated_access | scopes: ["read:accounts"]}
74 when action in [:favourited_by, :reblogged_by]
77 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
79 # Note: scope not present in Mastodon: read:bookmarks
80 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
82 # Note: scope not present in Mastodon: write:bookmarks
85 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
88 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
92 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
93 when action in ~w(reblog unreblog)a
98 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
99 when action in ~w(favourite unfavourite)a
102 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
104 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
106 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
109 GET `/api/v1/statuses?ids[]=1&ids[]=2`
111 `ids` query param is required
114 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{ids: ids} = params}}} =
123 |> Activity.all_by_ids_with_object()
124 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
126 render(conn, "index.json",
127 activities: activities,
130 with_muted: Map.get(params, :with_muted, false)
135 POST /api/v1/statuses
137 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
140 assigns: %{user: user},
142 open_api_spex: %{body_params: %{status: _, scheduled_at: scheduled_at} = params}
147 when not is_nil(scheduled_at) do
149 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
150 |> put_application(conn)
153 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
154 scheduled_at: scheduled_at
157 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
158 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
160 |> put_view(ScheduledActivityView)
161 |> render("show.json", scheduled_activity: scheduled_activity)
164 params = Map.drop(params, [:scheduled_at])
168 [Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
178 # Creates a regular status
181 private: %{open_api_spex: %{body_params: %{status: _}}}
190 assigns: %{user: _user},
191 private: %{open_api_spex: %{body_params: %{media_ids: _} = params}}
195 params = Map.put(params, :status, "")
199 [Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
206 %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn
209 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
210 |> put_application(conn)
212 with {:ok, activity} <- CommonAPI.post(user, params) do
213 try_render(conn, "show.json",
217 with_direct_conversation_id: true
220 {:error, {:reject, message}} ->
222 |> put_status(:unprocessable_entity)
223 |> json(%{error: message})
227 |> put_status(:unprocessable_entity)
228 |> json(%{error: message})
232 @doc "GET /api/v1/statuses/:id/history"
234 %{assigns: assigns, private: %{open_api_spex: %{params: %{id: id} = params}}} = conn,
237 with user = assigns[:user],
238 %Activity{} = activity <- Activity.get_by_id_with_object(id),
239 true <- Visibility.visible_for_user?(activity, user) do
240 try_render(conn, "history.json",
243 with_direct_conversation_id: true,
244 with_muted: Map.get(params, :with_muted, false)
247 _ -> {:error, :not_found}
251 @doc "GET /api/v1/statuses/:id/source"
252 def show_source(%{assigns: assigns, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
253 with user = assigns[:user],
254 %Activity{} = activity <- Activity.get_by_id_with_object(id),
255 true <- Visibility.visible_for_user?(activity, user) do
256 try_render(conn, "source.json",
261 _ -> {:error, :not_found}
265 @doc "PUT /api/v1/statuses/:id"
268 assigns: %{user: user},
269 private: %{open_api_spex: %{body_params: body_params, params: %{id: id} = params}}
273 with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
274 {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
275 {_, true} <- {:is_create, activity.data["type"] == "Create"},
276 actor <- Activity.user_actor(activity),
277 {_, true} <- {:own_status, actor.id == user.id},
278 changes <- body_params |> put_application(conn),
279 {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
280 {_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
281 try_render(conn, "show.json",
284 with_direct_conversation_id: true,
285 with_muted: Map.get(params, :with_muted, false)
288 {:own_status, _} -> {:error, :forbidden}
289 {:pipeline, _} -> {:error, :internal_server_error}
290 _ -> {:error, :not_found}
294 @doc "GET /api/v1/statuses/:id"
296 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id} = params}}} =
300 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
301 true <- Visibility.visible_for_user?(activity, user) do
302 try_render(conn, "show.json",
305 with_direct_conversation_id: true,
306 with_muted: Map.get(params, :with_muted, false)
309 _ -> {:error, :not_found}
313 @doc "DELETE /api/v1/statuses/:id"
314 def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
315 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
316 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
317 try_render(conn, "show.json",
320 with_direct_conversation_id: true,
324 _e -> {:error, :not_found}
328 @doc "POST /api/v1/statuses/:id/reblog"
331 assigns: %{user: user},
332 private: %{open_api_spex: %{body_params: params, params: %{id: ap_id_or_id}}}
336 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
337 %Activity{} = announce <- Activity.normalize(announce.data) do
338 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
342 @doc "POST /api/v1/statuses/:id/unreblog"
344 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
348 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
349 %Activity{} = activity <- Activity.get_by_id(activity_id) do
350 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
354 @doc "POST /api/v1/statuses/:id/favourite"
356 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
360 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
361 %Activity{} = activity <- Activity.get_by_id(activity_id) do
362 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
366 @doc "POST /api/v1/statuses/:id/unfavourite"
368 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
372 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
373 %Activity{} = activity <- Activity.get_by_id(activity_id) do
374 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
378 @doc "POST /api/v1/statuses/:id/pin"
380 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: ap_id_or_id}}}} =
384 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
385 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
387 {:error, :pinned_statuses_limit_reached} ->
388 {:error, "You have already pinned the maximum number of statuses"}
390 {:error, :ownership_error} ->
391 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
393 {:error, :visibility_error} ->
394 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
401 @doc "POST /api/v1/statuses/:id/unpin"
403 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: ap_id_or_id}}}} =
407 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
408 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
412 @doc "POST /api/v1/statuses/:id/bookmark"
415 assigns: %{user: user},
416 private: %{open_api_spex: %{body_params: body_params, params: %{id: id}}}
420 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
421 %User{} = user <- User.get_cached_by_nickname(user.nickname),
422 true <- Visibility.visible_for_user?(activity, user),
423 folder_id <- Map.get(body_params, :folder_id, nil),
425 if(folder_id && BookmarkFolder.belongs_to_user?(folder_id, user.id),
429 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id, folder_id) do
430 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
434 @doc "POST /api/v1/statuses/:id/unbookmark"
436 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
439 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
440 %User{} = user <- User.get_cached_by_nickname(user.nickname),
441 true <- Visibility.visible_for_user?(activity, user),
442 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
443 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
447 @doc "POST /api/v1/statuses/:id/mute"
448 def mute_conversation(
450 assigns: %{user: user},
451 private: %{open_api_spex: %{body_params: params, params: %{id: id}}}
455 with %Activity{} = activity <- Activity.get_by_id(id),
456 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
457 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
461 @doc "POST /api/v1/statuses/:id/unmute"
462 def unmute_conversation(
464 assigns: %{user: user},
465 private: %{open_api_spex: %{params: %{id: id}}}
469 with %Activity{} = activity <- Activity.get_by_id(id),
470 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
471 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
475 @doc "GET /api/v1/statuses/:id/favourited_by"
477 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
480 with true <- Pleroma.Config.get([:instance, :show_reactions]),
481 %Activity{} = activity <- Activity.get_by_id_with_object(id),
482 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
483 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
486 |> Ecto.Query.where([u], u.ap_id in ^likes)
488 |> Enum.filter(&(not User.blocks?(user, &1)))
491 |> put_view(AccountView)
492 |> render("index.json", for: user, users: users, as: :user)
494 {:visible, false} -> {:error, :not_found}
499 @doc "GET /api/v1/statuses/:id/reblogged_by"
501 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
504 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
505 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
506 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
507 Object.normalize(activity, fetch: false) do
510 |> Activity.Queries.by_type()
511 |> Ecto.Query.where([a], a.actor in ^announces)
512 # this is to use the index
513 |> Activity.Queries.by_object_id(ap_id)
515 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
516 |> Enum.map(& &1.actor)
521 |> Ecto.Query.where([u], u.ap_id in ^announces)
523 |> Enum.filter(&(not User.blocks?(user, &1)))
526 |> put_view(AccountView)
527 |> render("index.json", for: user, users: users, as: :user)
529 {:visible, false} -> {:error, :not_found}
534 @doc "GET /api/v1/statuses/:id/context"
536 %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
539 with %Activity{} = activity <- Activity.get_by_id(id) do
541 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
544 exclude_id: activity.id
547 render(conn, "context.json", activity: activity, activities: activities, user: user)
551 @doc "GET /api/v1/favourites"
553 %{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn,
556 activities = ActivityPub.fetch_favourites(user, params)
559 |> add_link_headers(activities)
560 |> render("index.json",
561 activities: activities,
567 @doc "GET /api/v1/bookmarks"
568 def bookmarks(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
569 user = User.get_cached_by_id(user.id)
570 folder_id = Map.get(params, :folder_id)
574 |> Bookmark.for_user_query(folder_id)
575 |> Pleroma.Pagination.fetch_paginated(params)
579 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
582 |> add_link_headers(bookmarks)
583 |> render("index.json",
584 activities: activities,
590 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
591 if user.disclose_client do
592 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
593 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
595 Map.put(params, :generator, nil)
599 defp put_application(params, _), do: Map.put(params, :generator, nil)