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
130 assigns: %{user: user},
131 body_params: %{status: _, scheduled_at: scheduled_at} = params
135 when not is_nil(scheduled_at) do
136 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
139 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
140 scheduled_at: scheduled_at
143 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
144 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
146 |> put_view(ScheduledActivityView)
147 |> render("show.json", scheduled_activity: scheduled_activity)
150 params = Map.drop(params, [:scheduled_at])
151 create(%Plug.Conn{conn | body_params: params}, %{})
159 # Creates a regular status
160 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
161 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
163 with {:ok, activity} <- CommonAPI.post(user, params) do
164 try_render(conn, "show.json",
168 with_direct_conversation_id: true
171 {:error, {:reject, message}} ->
173 |> put_status(:unprocessable_entity)
174 |> json(%{error: message})
178 |> put_status(:unprocessable_entity)
179 |> json(%{error: message})
183 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
184 params = Map.put(params, :status, "")
185 create(%Plug.Conn{conn | body_params: params}, %{})
188 @doc "GET /api/v1/statuses/:id"
189 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
190 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
191 true <- Visibility.visible_for_user?(activity, user) do
192 try_render(conn, "show.json",
195 with_direct_conversation_id: true
198 _ -> {:error, :not_found}
202 @doc "DELETE /api/v1/statuses/:id"
203 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
204 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
205 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
206 try_render(conn, "show.json",
209 with_direct_conversation_id: true,
213 _e -> {:error, :not_found}
217 @doc "POST /api/v1/statuses/:id/reblog"
218 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
219 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
220 %Activity{} = announce <- Activity.normalize(announce.data) do
221 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
225 @doc "POST /api/v1/statuses/:id/unreblog"
226 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
227 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
228 %Activity{} = activity <- Activity.get_by_id(activity_id) do
229 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
233 @doc "POST /api/v1/statuses/:id/favourite"
234 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
235 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
236 %Activity{} = activity <- Activity.get_by_id(activity_id) do
237 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
241 @doc "POST /api/v1/statuses/:id/unfavourite"
242 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
243 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
244 %Activity{} = activity <- Activity.get_by_id(activity_id) do
245 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
249 @doc "POST /api/v1/statuses/:id/pin"
250 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
251 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
252 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
256 @doc "POST /api/v1/statuses/:id/unpin"
257 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
258 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
259 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
263 @doc "POST /api/v1/statuses/:id/bookmark"
264 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
265 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
266 %User{} = user <- User.get_cached_by_nickname(user.nickname),
267 true <- Visibility.visible_for_user?(activity, user),
268 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
269 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
273 @doc "POST /api/v1/statuses/:id/unbookmark"
274 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
275 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
276 %User{} = user <- User.get_cached_by_nickname(user.nickname),
277 true <- Visibility.visible_for_user?(activity, user),
278 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
279 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
283 @doc "POST /api/v1/statuses/:id/mute"
284 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
285 with %Activity{} = activity <- Activity.get_by_id(id),
286 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
287 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
291 @doc "POST /api/v1/statuses/:id/unmute"
292 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
293 with %Activity{} = activity <- Activity.get_by_id(id),
294 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
295 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
299 @doc "GET /api/v1/statuses/:id/card"
300 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
301 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
302 with %Activity{} = activity <- Activity.get_by_id(status_id),
303 true <- Visibility.visible_for_user?(activity, user) do
304 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
305 render(conn, "card.json", data)
307 _ -> render_error(conn, :not_found, "Record not found")
311 @doc "GET /api/v1/statuses/:id/favourited_by"
312 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
313 with true <- Pleroma.Config.get([:instance, :show_reactions]),
314 %Activity{} = activity <- Activity.get_by_id_with_object(id),
315 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
316 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
319 |> Ecto.Query.where([u], u.ap_id in ^likes)
321 |> Enum.filter(&(not User.blocks?(user, &1)))
324 |> put_view(AccountView)
325 |> render("index.json", for: user, users: users, as: :user)
327 {:visible, false} -> {:error, :not_found}
332 @doc "GET /api/v1/statuses/:id/reblogged_by"
333 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
334 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
335 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
336 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
337 Object.normalize(activity) do
340 |> Activity.Queries.by_type()
341 |> Ecto.Query.where([a], a.actor in ^announces)
342 # this is to use the index
343 |> Activity.Queries.by_object_id(ap_id)
345 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
346 |> Enum.map(& &1.actor)
351 |> Ecto.Query.where([u], u.ap_id in ^announces)
353 |> Enum.filter(&(not User.blocks?(user, &1)))
356 |> put_view(AccountView)
357 |> render("index.json", for: user, users: users, as: :user)
359 {:visible, false} -> {:error, :not_found}
364 @doc "GET /api/v1/statuses/:id/context"
365 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
366 with %Activity{} = activity <- Activity.get_by_id(id) do
368 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
371 exclude_id: activity.id
374 render(conn, "context.json", activity: activity, activities: activities, user: user)
378 @doc "GET /api/v1/favourites"
379 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
380 activities = ActivityPub.fetch_favourites(user, params)
383 |> add_link_headers(activities)
384 |> render("index.json",
385 activities: activities,
391 @doc "GET /api/v1/bookmarks"
392 def bookmarks(%{assigns: %{user: user}} = conn, params) do
393 user = User.get_cached_by_id(user.id)
397 |> Bookmark.for_user_query()
398 |> Pleroma.Pagination.fetch_paginated(params)
402 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
405 |> add_link_headers(bookmarks)
406 |> render("index.json",
407 activities: activities,