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
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.OAuth.Token
25 alias Pleroma.Web.Plugs.OAuthScopesPlug
26 alias Pleroma.Web.Plugs.RateLimiter
28 plug(Pleroma.Web.ApiSpec.CastAndValidate)
30 plug(:skip_public_check when action in [:index, :show])
32 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
36 %{@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
113 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
119 |> Activity.all_by_ids_with_object()
120 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
122 render(conn, "index.json",
123 activities: activities,
126 with_muted: Map.get(params, :with_muted, false)
131 POST /api/v1/statuses
133 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
136 assigns: %{user: user},
137 body_params: %{status: _, scheduled_at: scheduled_at} = params
141 when not is_nil(scheduled_at) do
143 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
144 |> put_application(conn)
147 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
148 scheduled_at: scheduled_at
151 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
152 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
154 |> put_view(ScheduledActivityView)
155 |> render("show.json", scheduled_activity: scheduled_activity)
158 params = Map.drop(params, [:scheduled_at])
159 create(%Plug.Conn{conn | body_params: params}, %{})
166 # Creates a regular status
167 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
169 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
170 |> put_application(conn)
172 with {:ok, activity} <- CommonAPI.post(user, params) do
173 try_render(conn, "show.json",
177 with_direct_conversation_id: true
180 {:error, {:reject, message}} ->
182 |> put_status(:unprocessable_entity)
183 |> json(%{error: message})
187 |> put_status(:unprocessable_entity)
188 |> json(%{error: message})
192 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
193 params = Map.put(params, :status, "")
194 create(%Plug.Conn{conn | body_params: params}, %{})
197 @doc "GET /api/v1/statuses/:id/history"
198 def show_history(%{assigns: assigns} = conn, %{id: id} = params) do
199 with user = assigns[:user],
200 %Activity{} = activity <- Activity.get_by_id_with_object(id),
201 true <- Visibility.visible_for_user?(activity, user) do
202 try_render(conn, "history.json",
205 with_direct_conversation_id: true,
206 with_muted: Map.get(params, :with_muted, false)
209 _ -> {:error, :not_found}
213 @doc "GET /api/v1/statuses/:id/source"
214 def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do
215 with user = assigns[:user],
216 %Activity{} = activity <- Activity.get_by_id_with_object(id),
217 true <- Visibility.visible_for_user?(activity, user) do
218 try_render(conn, "source.json",
223 _ -> {:error, :not_found}
227 @doc "PUT /api/v1/statuses/:id"
228 def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
229 with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
230 {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
231 {_, true} <- {:is_create, activity.data["type"] == "Create"},
232 actor <- Activity.user_actor(activity),
233 {_, true} <- {:own_status, actor.id == user.id},
234 changes <- body_params |> put_application(conn),
235 {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
236 {_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
237 try_render(conn, "show.json",
240 with_direct_conversation_id: true,
241 with_muted: Map.get(params, :with_muted, false)
244 {:own_status, _} -> {:error, :forbidden}
245 {:pipeline, _} -> {:error, :internal_server_error}
246 _ -> {:error, :not_found}
250 @doc "GET /api/v1/statuses/:id"
251 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
252 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
253 true <- Visibility.visible_for_user?(activity, user) do
254 try_render(conn, "show.json",
257 with_direct_conversation_id: true,
258 with_muted: Map.get(params, :with_muted, false)
261 _ -> {:error, :not_found}
265 @doc "DELETE /api/v1/statuses/:id"
266 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
267 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
268 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
269 try_render(conn, "show.json",
272 with_direct_conversation_id: true,
276 _e -> {:error, :not_found}
280 @doc "POST /api/v1/statuses/:id/reblog"
281 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
282 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
283 %Activity{} = announce <- Activity.normalize(announce.data) do
284 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
288 @doc "POST /api/v1/statuses/:id/unreblog"
289 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
290 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
291 %Activity{} = activity <- Activity.get_by_id(activity_id) do
292 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
296 @doc "POST /api/v1/statuses/:id/favourite"
297 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
298 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
299 %Activity{} = activity <- Activity.get_by_id(activity_id) do
300 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
304 @doc "POST /api/v1/statuses/:id/unfavourite"
305 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
306 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
307 %Activity{} = activity <- Activity.get_by_id(activity_id) do
308 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
312 @doc "POST /api/v1/statuses/:id/pin"
313 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
314 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
315 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
317 {:error, :pinned_statuses_limit_reached} ->
318 {:error, "You have already pinned the maximum number of statuses"}
320 {:error, :ownership_error} ->
321 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
323 {:error, :visibility_error} ->
324 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
331 @doc "POST /api/v1/statuses/:id/unpin"
332 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
333 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
334 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
338 @doc "POST /api/v1/statuses/:id/bookmark"
339 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
340 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
341 %User{} = user <- User.get_cached_by_nickname(user.nickname),
342 true <- Visibility.visible_for_user?(activity, user),
343 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
344 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
348 @doc "POST /api/v1/statuses/:id/unbookmark"
349 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
350 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
351 %User{} = user <- User.get_cached_by_nickname(user.nickname),
352 true <- Visibility.visible_for_user?(activity, user),
353 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
354 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
358 @doc "POST /api/v1/statuses/:id/mute"
359 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
360 with %Activity{} = activity <- Activity.get_by_id(id),
361 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
362 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
366 @doc "POST /api/v1/statuses/:id/unmute"
367 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
368 with %Activity{} = activity <- Activity.get_by_id(id),
369 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
370 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
374 @doc "GET /api/v1/statuses/:id/card"
375 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
376 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
377 with %Activity{} = activity <- Activity.get_by_id(status_id),
378 true <- Visibility.visible_for_user?(activity, user) do
379 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
380 render(conn, "card.json", data)
382 _ -> render_error(conn, :not_found, "Record not found")
386 @doc "GET /api/v1/statuses/:id/favourited_by"
387 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
388 with true <- Pleroma.Config.get([:instance, :show_reactions]),
389 %Activity{} = activity <- Activity.get_by_id_with_object(id),
390 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
391 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
394 |> Ecto.Query.where([u], u.ap_id in ^likes)
396 |> Enum.filter(&(not User.blocks?(user, &1)))
399 |> put_view(AccountView)
400 |> render("index.json", for: user, users: users, as: :user)
402 {:visible, false} -> {:error, :not_found}
407 @doc "GET /api/v1/statuses/:id/reblogged_by"
408 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
409 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
410 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
411 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
412 Object.normalize(activity, fetch: false) do
415 |> Activity.Queries.by_type()
416 |> Ecto.Query.where([a], a.actor in ^announces)
417 # this is to use the index
418 |> Activity.Queries.by_object_id(ap_id)
420 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
421 |> Enum.map(& &1.actor)
426 |> Ecto.Query.where([u], u.ap_id in ^announces)
428 |> Enum.filter(&(not User.blocks?(user, &1)))
431 |> put_view(AccountView)
432 |> render("index.json", for: user, users: users, as: :user)
434 {:visible, false} -> {:error, :not_found}
439 @doc "GET /api/v1/statuses/:id/context"
440 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
441 with %Activity{} = activity <- Activity.get_by_id(id) do
443 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
446 exclude_id: activity.id
449 render(conn, "context.json", activity: activity, activities: activities, user: user)
453 @doc "GET /api/v1/favourites"
454 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
455 activities = ActivityPub.fetch_favourites(user, params)
458 |> add_link_headers(activities)
459 |> render("index.json",
460 activities: activities,
466 @doc "GET /api/v1/bookmarks"
467 def bookmarks(%{assigns: %{user: user}} = conn, params) do
468 user = User.get_cached_by_id(user.id)
472 |> Bookmark.for_user_query()
473 |> Pleroma.Pagination.fetch_paginated(params)
477 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
480 |> add_link_headers(bookmarks)
481 |> render("index.json",
482 activities: activities,
488 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
489 if user.disclose_client do
490 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
491 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
493 Map.put(params, :generator, nil)
497 defp put_application(params, _), do: Map.put(params, :generator, nil)