1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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
17 alias Pleroma.ScheduledActivity
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Visibility
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.MastodonAPI.AccountView
23 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
24 alias Pleroma.Web.Plugs.OAuthScopesPlug
25 alias Pleroma.Web.Plugs.RateLimiter
27 plug(Pleroma.Web.ApiSpec.CastAndValidate)
31 Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show]
34 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
38 %{@unauthenticated_access | scopes: ["read:statuses"]}
49 %{scopes: ["write:statuses"]}
58 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
62 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
67 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
72 %{@unauthenticated_access | scopes: ["read:accounts"]}
73 when action in [:favourited_by, :reblogged_by]
76 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
78 # Note: scope not present in Mastodon: read:bookmarks
79 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
81 # Note: scope not present in Mastodon: write:bookmarks
84 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
87 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
91 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
92 when action in ~w(reblog unreblog)a
97 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
98 when action in ~w(favourite unfavourite)a
101 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
103 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
105 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
108 GET `/api/v1/statuses?ids[]=1&ids[]=2`
110 `ids` query param is required
112 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
118 |> Activity.all_by_ids_with_object()
119 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
121 render(conn, "index.json",
122 activities: activities,
125 with_muted: Map.get(params, :with_muted, false)
130 POST /api/v1/statuses
132 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
135 assigns: %{user: user, token: %{app_id: app_id}},
136 body_params: %{status: _, scheduled_at: scheduled_at} = params
140 when not is_nil(scheduled_at) do
142 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
143 |> add_application(app_id)
146 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
147 scheduled_at: scheduled_at
150 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
151 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
153 |> put_view(ScheduledActivityView)
154 |> render("show.json", scheduled_activity: scheduled_activity)
157 params = Map.drop(params, [:scheduled_at])
158 create(%Plug.Conn{conn | body_params: params}, %{})
165 # Creates a regular status
167 %{assigns: %{user: user, token: %{app_id: app_id}}, body_params: %{status: _} = params} =
172 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
173 |> add_application(app_id)
175 with {:ok, activity} <- CommonAPI.post(user, params) do
176 try_render(conn, "show.json",
180 with_direct_conversation_id: true
183 {:error, {:reject, message}} ->
185 |> put_status(:unprocessable_entity)
186 |> json(%{error: message})
190 |> put_status(:unprocessable_entity)
191 |> json(%{error: message})
195 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
196 params = Map.put(params, :status, "")
197 create(%Plug.Conn{conn | body_params: params}, %{})
200 @doc "GET /api/v1/statuses/:id"
201 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
202 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
203 true <- Visibility.visible_for_user?(activity, user) do
204 try_render(conn, "show.json",
207 with_direct_conversation_id: true,
208 with_muted: Map.get(params, :with_muted, false)
211 _ -> {:error, :not_found}
215 @doc "DELETE /api/v1/statuses/:id"
216 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
217 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
218 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
219 try_render(conn, "show.json",
222 with_direct_conversation_id: true,
226 _e -> {:error, :not_found}
230 @doc "POST /api/v1/statuses/:id/reblog"
231 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
232 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
233 %Activity{} = announce <- Activity.normalize(announce.data) do
234 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
238 @doc "POST /api/v1/statuses/:id/unreblog"
239 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
240 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
241 %Activity{} = activity <- Activity.get_by_id(activity_id) do
242 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
246 @doc "POST /api/v1/statuses/:id/favourite"
247 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
248 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
249 %Activity{} = activity <- Activity.get_by_id(activity_id) do
250 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
254 @doc "POST /api/v1/statuses/:id/unfavourite"
255 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
256 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
257 %Activity{} = activity <- Activity.get_by_id(activity_id) do
258 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
262 @doc "POST /api/v1/statuses/:id/pin"
263 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
264 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
265 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
269 @doc "POST /api/v1/statuses/:id/unpin"
270 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
271 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
272 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
276 @doc "POST /api/v1/statuses/:id/bookmark"
277 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
278 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
279 %User{} = user <- User.get_cached_by_nickname(user.nickname),
280 true <- Visibility.visible_for_user?(activity, user),
281 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
282 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
286 @doc "POST /api/v1/statuses/:id/unbookmark"
287 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
288 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
289 %User{} = user <- User.get_cached_by_nickname(user.nickname),
290 true <- Visibility.visible_for_user?(activity, user),
291 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
292 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
296 @doc "POST /api/v1/statuses/:id/mute"
297 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
298 with %Activity{} = activity <- Activity.get_by_id(id),
299 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
300 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
304 @doc "POST /api/v1/statuses/:id/unmute"
305 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
306 with %Activity{} = activity <- Activity.get_by_id(id),
307 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
308 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
312 @doc "GET /api/v1/statuses/:id/card"
313 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
314 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
315 with %Activity{} = activity <- Activity.get_by_id(status_id),
316 true <- Visibility.visible_for_user?(activity, user) do
317 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
318 render(conn, "card.json", data)
320 _ -> render_error(conn, :not_found, "Record not found")
324 @doc "GET /api/v1/statuses/:id/favourited_by"
325 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
326 with true <- Pleroma.Config.get([:instance, :show_reactions]),
327 %Activity{} = activity <- Activity.get_by_id_with_object(id),
328 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
329 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
332 |> Ecto.Query.where([u], u.ap_id in ^likes)
334 |> Enum.filter(&(not User.blocks?(user, &1)))
337 |> put_view(AccountView)
338 |> render("index.json", for: user, users: users, as: :user)
340 {:visible, false} -> {:error, :not_found}
345 @doc "GET /api/v1/statuses/:id/reblogged_by"
346 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
347 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
348 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
349 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
350 Object.normalize(activity, fetch: false) do
353 |> Activity.Queries.by_type()
354 |> Ecto.Query.where([a], a.actor in ^announces)
355 # this is to use the index
356 |> Activity.Queries.by_object_id(ap_id)
358 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
359 |> Enum.map(& &1.actor)
364 |> Ecto.Query.where([u], u.ap_id in ^announces)
366 |> Enum.filter(&(not User.blocks?(user, &1)))
369 |> put_view(AccountView)
370 |> render("index.json", for: user, users: users, as: :user)
372 {:visible, false} -> {:error, :not_found}
377 @doc "GET /api/v1/statuses/:id/context"
378 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
379 with %Activity{} = activity <- Activity.get_by_id(id) do
381 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
384 exclude_id: activity.id
387 render(conn, "context.json", activity: activity, activities: activities, user: user)
391 @doc "GET /api/v1/favourites"
392 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
393 activities = ActivityPub.fetch_favourites(user, params)
396 |> add_link_headers(activities)
397 |> render("index.json",
398 activities: activities,
404 @doc "GET /api/v1/bookmarks"
405 def bookmarks(%{assigns: %{user: user}} = conn, params) do
406 user = User.get_cached_by_id(user.id)
410 |> Bookmark.for_user_query()
411 |> Pleroma.Pagination.fetch_paginated(params)
415 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
418 |> add_link_headers(bookmarks)
419 |> render("index.json",
420 activities: activities,
426 defp add_application(params, app_id) do
427 params |> Map.put(:application, Pleroma.Web.OAuth.App.get_app_by_id(app_id))