1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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
16 alias Pleroma.Plugs.OAuthScopesPlug
17 alias Pleroma.Plugs.RateLimiter
19 alias Pleroma.ScheduledActivity
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.ActivityPub.Visibility
23 alias Pleroma.Web.CommonAPI
24 alias Pleroma.Web.MastodonAPI.AccountView
25 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
27 plug(Pleroma.Web.ApiSpec.CastAndValidate)
28 plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
30 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
34 %{@unauthenticated_access | scopes: ["read:statuses"]}
45 %{scopes: ["write:statuses"]}
54 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
58 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
63 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
68 %{@unauthenticated_access | scopes: ["read:accounts"]}
69 when action in [:favourited_by, :reblogged_by]
72 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
74 # Note: scope not present in Mastodon: read:bookmarks
75 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
77 # Note: scope not present in Mastodon: write:bookmarks
80 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
83 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
87 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
88 when action in ~w(reblog unreblog)a
93 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
94 when action in ~w(favourite unfavourite)a
97 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
99 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
101 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
104 GET `/api/v1/statuses?ids[]=1&ids[]=2`
106 `ids` query param is required
108 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
114 |> Activity.all_by_ids_with_object()
115 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
117 render(conn, "index.json",
118 activities: activities,
125 POST /api/v1/statuses
127 Creates a scheduled status when `scheduled_at` param is present and it's far enough
131 assigns: %{user: user},
132 body_params: %{status: _, scheduled_at: scheduled_at} = params
136 when not is_nil(scheduled_at) do
137 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
140 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
141 scheduled_at: scheduled_at
144 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
145 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
147 |> put_view(ScheduledActivityView)
148 |> render("show.json", scheduled_activity: scheduled_activity)
151 params = Map.drop(params, [:scheduled_at])
152 create(%Plug.Conn{conn | body_params: params}, %{})
160 POST /api/v1/statuses
162 Creates a regular status
164 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
165 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
167 with {:ok, activity} <- CommonAPI.post(user, params) do
168 try_render(conn, "show.json",
172 with_direct_conversation_id: true
177 |> put_status(:unprocessable_entity)
178 |> json(%{error: message})
182 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
183 params = Map.put(params, :status, "")
184 create(%Plug.Conn{conn | body_params: params}, %{})
187 @doc "GET /api/v1/statuses/:id"
188 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
189 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
190 true <- Visibility.visible_for_user?(activity, user) do
191 try_render(conn, "show.json",
194 with_direct_conversation_id: true
197 _ -> {:error, :not_found}
201 @doc "DELETE /api/v1/statuses/:id"
202 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
203 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
204 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
205 try_render(conn, "show.json",
208 with_direct_conversation_id: true,
212 _e -> {:error, :not_found}
216 @doc "POST /api/v1/statuses/:id/reblog"
217 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
218 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
219 %Activity{} = announce <- Activity.normalize(announce.data) do
220 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
224 @doc "POST /api/v1/statuses/:id/unreblog"
225 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
226 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
227 %Activity{} = activity <- Activity.get_by_id(activity_id) do
228 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
232 @doc "POST /api/v1/statuses/:id/favourite"
233 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
234 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
235 %Activity{} = activity <- Activity.get_by_id(activity_id) do
236 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
240 @doc "POST /api/v1/statuses/:id/unfavourite"
241 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
242 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
243 %Activity{} = activity <- Activity.get_by_id(activity_id) do
244 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
248 @doc "POST /api/v1/statuses/:id/pin"
249 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
250 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
251 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
255 @doc "POST /api/v1/statuses/:id/unpin"
256 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
257 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
258 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
262 @doc "POST /api/v1/statuses/:id/bookmark"
263 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
264 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
265 %User{} = user <- User.get_cached_by_nickname(user.nickname),
266 true <- Visibility.visible_for_user?(activity, user),
267 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
268 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
272 @doc "POST /api/v1/statuses/:id/unbookmark"
273 def unbookmark(%{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.destroy(user.id, activity.id) do
278 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
282 @doc "POST /api/v1/statuses/:id/mute"
283 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
284 with %Activity{} = activity <- Activity.get_by_id(id),
285 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
286 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
290 @doc "POST /api/v1/statuses/:id/unmute"
291 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
292 with %Activity{} = activity <- Activity.get_by_id(id),
293 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
294 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
298 @doc "GET /api/v1/statuses/:id/card"
299 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
300 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
301 with %Activity{} = activity <- Activity.get_by_id(status_id),
302 true <- Visibility.visible_for_user?(activity, user) do
303 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
304 render(conn, "card.json", data)
306 _ -> render_error(conn, :not_found, "Record not found")
310 @doc "GET /api/v1/statuses/:id/favourited_by"
311 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
312 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
313 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
314 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
317 |> Ecto.Query.where([u], u.ap_id in ^likes)
319 |> Enum.filter(&(not User.blocks?(user, &1)))
322 |> put_view(AccountView)
323 |> render("index.json", for: user, users: users, as: :user)
325 {:visible, false} -> {:error, :not_found}
330 @doc "GET /api/v1/statuses/:id/reblogged_by"
331 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
332 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
333 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
334 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
335 Object.normalize(activity) do
338 |> Activity.Queries.by_type()
339 |> Ecto.Query.where([a], a.actor in ^announces)
340 # this is to use the index
341 |> Activity.Queries.by_object_id(ap_id)
343 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
344 |> Enum.map(& &1.actor)
349 |> Ecto.Query.where([u], u.ap_id in ^announces)
351 |> Enum.filter(&(not User.blocks?(user, &1)))
354 |> put_view(AccountView)
355 |> render("index.json", for: user, users: users, as: :user)
357 {:visible, false} -> {:error, :not_found}
362 @doc "GET /api/v1/statuses/:id/context"
363 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
364 with %Activity{} = activity <- Activity.get_by_id(id) do
366 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
369 exclude_id: activity.id
372 render(conn, "context.json", activity: activity, activities: activities, user: user)
376 @doc "GET /api/v1/favourites"
377 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
378 activities = ActivityPub.fetch_favourites(user, params)
381 |> add_link_headers(activities)
382 |> render("index.json",
383 activities: activities,
389 @doc "GET /api/v1/bookmarks"
390 def bookmarks(%{assigns: %{user: user}} = conn, params) do
391 user = User.get_cached_by_id(user.id)
395 |> Bookmark.for_user_query()
396 |> Pleroma.Pagination.fetch_paginated(params)
400 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
403 |> add_link_headers(bookmarks)
404 |> render("index.json",
405 activities: activities,