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},
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 |> put_application(conn)
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
166 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
168 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
169 |> put_application(conn)
171 with {:ok, activity} <- CommonAPI.post(user, params) do
172 try_render(conn, "show.json",
176 with_direct_conversation_id: true
179 {:error, {:reject, message}} ->
181 |> put_status(:unprocessable_entity)
182 |> json(%{error: message})
186 |> put_status(:unprocessable_entity)
187 |> json(%{error: message})
191 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
192 params = Map.put(params, :status, "")
193 create(%Plug.Conn{conn | body_params: params}, %{})
196 @doc "GET /api/v1/statuses/:id"
197 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
198 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
199 true <- Visibility.visible_for_user?(activity, user) do
200 try_render(conn, "show.json",
203 with_direct_conversation_id: true,
204 with_muted: Map.get(params, :with_muted, false)
207 _ -> {:error, :not_found}
211 @doc "DELETE /api/v1/statuses/:id"
212 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
213 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
214 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
215 try_render(conn, "show.json",
218 with_direct_conversation_id: true,
222 _e -> {:error, :not_found}
226 @doc "POST /api/v1/statuses/:id/reblog"
227 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
228 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
229 %Activity{} = announce <- Activity.normalize(announce.data) do
230 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
234 @doc "POST /api/v1/statuses/:id/unreblog"
235 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
236 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
237 %Activity{} = activity <- Activity.get_by_id(activity_id) do
238 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
242 @doc "POST /api/v1/statuses/:id/favourite"
243 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
244 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
245 %Activity{} = activity <- Activity.get_by_id(activity_id) do
246 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
250 @doc "POST /api/v1/statuses/:id/unfavourite"
251 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
252 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
253 %Activity{} = activity <- Activity.get_by_id(activity_id) do
254 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
258 @doc "POST /api/v1/statuses/:id/pin"
259 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
260 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
261 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
265 @doc "POST /api/v1/statuses/:id/unpin"
266 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
267 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
268 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
272 @doc "POST /api/v1/statuses/:id/bookmark"
273 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
274 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
275 %User{} = user <- User.get_cached_by_nickname(user.nickname),
276 true <- Visibility.visible_for_user?(activity, user),
277 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
278 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
282 @doc "POST /api/v1/statuses/:id/unbookmark"
283 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
284 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
285 %User{} = user <- User.get_cached_by_nickname(user.nickname),
286 true <- Visibility.visible_for_user?(activity, user),
287 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
288 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
292 @doc "POST /api/v1/statuses/:id/mute"
293 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
294 with %Activity{} = activity <- Activity.get_by_id(id),
295 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
296 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
300 @doc "POST /api/v1/statuses/:id/unmute"
301 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
302 with %Activity{} = activity <- Activity.get_by_id(id),
303 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
304 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
308 @doc "GET /api/v1/statuses/:id/card"
309 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
310 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
311 with %Activity{} = activity <- Activity.get_by_id(status_id),
312 true <- Visibility.visible_for_user?(activity, user) do
313 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
314 render(conn, "card.json", data)
316 _ -> render_error(conn, :not_found, "Record not found")
320 @doc "GET /api/v1/statuses/:id/favourited_by"
321 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
322 with true <- Pleroma.Config.get([:instance, :show_reactions]),
323 %Activity{} = activity <- Activity.get_by_id_with_object(id),
324 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
325 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
328 |> Ecto.Query.where([u], u.ap_id in ^likes)
330 |> Enum.filter(&(not User.blocks?(user, &1)))
333 |> put_view(AccountView)
334 |> render("index.json", for: user, users: users, as: :user)
336 {:visible, false} -> {:error, :not_found}
341 @doc "GET /api/v1/statuses/:id/reblogged_by"
342 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
343 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
344 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
345 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
346 Object.normalize(activity, fetch: false) do
349 |> Activity.Queries.by_type()
350 |> Ecto.Query.where([a], a.actor in ^announces)
351 # this is to use the index
352 |> Activity.Queries.by_object_id(ap_id)
354 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
355 |> Enum.map(& &1.actor)
360 |> Ecto.Query.where([u], u.ap_id in ^announces)
362 |> Enum.filter(&(not User.blocks?(user, &1)))
365 |> put_view(AccountView)
366 |> render("index.json", for: user, users: users, as: :user)
368 {:visible, false} -> {:error, :not_found}
373 @doc "GET /api/v1/statuses/:id/context"
374 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
375 with %Activity{} = activity <- Activity.get_by_id(id) do
377 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
380 exclude_id: activity.id
383 render(conn, "context.json", activity: activity, activities: activities, user: user)
387 @doc "GET /api/v1/favourites"
388 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
389 activities = ActivityPub.fetch_favourites(user, params)
392 |> add_link_headers(activities)
393 |> render("index.json",
394 activities: activities,
400 @doc "GET /api/v1/bookmarks"
401 def bookmarks(%{assigns: %{user: user}} = conn, params) do
402 user = User.get_cached_by_id(user.id)
406 |> Bookmark.for_user_query()
407 |> Pleroma.Pagination.fetch_paginated(params)
411 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
414 |> add_link_headers(bookmarks)
415 |> render("index.json",
416 activities: activities,
422 defp put_application(params, %{assigns: %{token: %{app_id: app_id}}} = _conn) do
423 params |> Map.put(:application, Pleroma.Web.OAuth.App.get_app_by_id(app_id))
426 defp put_application(params, _), do: Map.put(params, :application, %{name: "Web", website: nil})