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
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,
129 POST /api/v1/statuses
131 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
134 assigns: %{user: user},
135 body_params: %{status: _, scheduled_at: scheduled_at} = params
139 when not is_nil(scheduled_at) do
140 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
143 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
144 scheduled_at: scheduled_at
147 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
148 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
150 |> put_view(ScheduledActivityView)
151 |> render("show.json", scheduled_activity: scheduled_activity)
154 params = Map.drop(params, [:scheduled_at])
155 create(%Plug.Conn{conn | body_params: params}, %{})
162 # Creates a regular status
163 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
164 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
166 with {:ok, activity} <- CommonAPI.post(user, params) do
167 try_render(conn, "show.json",
171 with_direct_conversation_id: true
174 {:error, {:reject, message}} ->
176 |> put_status(:unprocessable_entity)
177 |> json(%{error: message})
181 |> put_status(:unprocessable_entity)
182 |> json(%{error: message})
186 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
187 params = Map.put(params, :status, "")
188 create(%Plug.Conn{conn | body_params: params}, %{})
191 @doc "GET /api/v1/statuses/:id"
192 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
193 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
194 true <- Visibility.visible_for_user?(activity, user) do
195 try_render(conn, "show.json",
198 with_direct_conversation_id: true
201 _ -> {:error, :not_found}
205 @doc "DELETE /api/v1/statuses/:id"
206 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
207 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
208 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
209 try_render(conn, "show.json",
212 with_direct_conversation_id: true,
216 _e -> {:error, :not_found}
220 @doc "POST /api/v1/statuses/:id/reblog"
221 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
222 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
223 %Activity{} = announce <- Activity.normalize(announce.data) do
224 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
228 @doc "POST /api/v1/statuses/:id/unreblog"
229 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
230 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
231 %Activity{} = activity <- Activity.get_by_id(activity_id) do
232 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
236 @doc "POST /api/v1/statuses/:id/favourite"
237 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
238 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
239 %Activity{} = activity <- Activity.get_by_id(activity_id) do
240 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
244 @doc "POST /api/v1/statuses/:id/unfavourite"
245 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
246 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
247 %Activity{} = activity <- Activity.get_by_id(activity_id) do
248 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
252 @doc "POST /api/v1/statuses/:id/pin"
253 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
254 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
255 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
259 @doc "POST /api/v1/statuses/:id/unpin"
260 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
261 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
262 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
266 @doc "POST /api/v1/statuses/:id/bookmark"
267 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
268 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
269 %User{} = user <- User.get_cached_by_nickname(user.nickname),
270 true <- Visibility.visible_for_user?(activity, user),
271 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
272 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
276 @doc "POST /api/v1/statuses/:id/unbookmark"
277 def unbookmark(%{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.destroy(user.id, activity.id) do
282 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
286 @doc "POST /api/v1/statuses/:id/mute"
287 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
288 with %Activity{} = activity <- Activity.get_by_id(id),
289 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
290 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
294 @doc "POST /api/v1/statuses/:id/unmute"
295 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
296 with %Activity{} = activity <- Activity.get_by_id(id),
297 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
298 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
302 @doc "GET /api/v1/statuses/:id/card"
303 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
304 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
305 with %Activity{} = activity <- Activity.get_by_id(status_id),
306 true <- Visibility.visible_for_user?(activity, user) do
307 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
308 render(conn, "card.json", data)
310 _ -> render_error(conn, :not_found, "Record not found")
314 @doc "GET /api/v1/statuses/:id/favourited_by"
315 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
316 with true <- Pleroma.Config.get([:instance, :show_reactions]),
317 %Activity{} = activity <- Activity.get_by_id_with_object(id),
318 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
319 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
322 |> Ecto.Query.where([u], u.ap_id in ^likes)
324 |> Enum.filter(&(not User.blocks?(user, &1)))
327 |> put_view(AccountView)
328 |> render("index.json", for: user, users: users, as: :user)
330 {:visible, false} -> {:error, :not_found}
335 @doc "GET /api/v1/statuses/:id/reblogged_by"
336 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
337 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
338 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
339 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
340 Object.normalize(activity) do
343 |> Activity.Queries.by_type()
344 |> Ecto.Query.where([a], a.actor in ^announces)
345 # this is to use the index
346 |> Activity.Queries.by_object_id(ap_id)
348 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
349 |> Enum.map(& &1.actor)
354 |> Ecto.Query.where([u], u.ap_id in ^announces)
356 |> Enum.filter(&(not User.blocks?(user, &1)))
359 |> put_view(AccountView)
360 |> render("index.json", for: user, users: users, as: :user)
362 {:visible, false} -> {:error, :not_found}
367 @doc "GET /api/v1/statuses/:id/context"
368 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
369 with %Activity{} = activity <- Activity.get_by_id(id) do
371 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
374 exclude_id: activity.id
377 render(conn, "context.json", activity: activity, activities: activities, user: user)
381 @doc "GET /api/v1/favourites"
382 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
383 activities = ActivityPub.fetch_favourites(user, params)
386 |> add_link_headers(activities)
387 |> render("index.json",
388 activities: activities,
394 @doc "GET /api/v1/bookmarks"
395 def bookmarks(%{assigns: %{user: user}} = conn, params) do
396 user = User.get_cached_by_id(user.id)
400 |> Bookmark.for_user_query()
401 |> Pleroma.Pagination.fetch_paginated(params)
405 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
408 |> add_link_headers(bookmarks)
409 |> render("index.json",
410 activities: activities,