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
175 {:error, {:reject, message}} ->
177 |> put_status(:unprocessable_entity)
178 |> json(%{error: message})
182 |> put_status(:unprocessable_entity)
183 |> json(%{error: message})
187 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
188 params = Map.put(params, :status, "")
189 create(%Plug.Conn{conn | body_params: params}, %{})
192 @doc "GET /api/v1/statuses/:id"
193 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
194 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
195 true <- Visibility.visible_for_user?(activity, user) do
196 try_render(conn, "show.json",
199 with_direct_conversation_id: true
202 _ -> {:error, :not_found}
206 @doc "DELETE /api/v1/statuses/:id"
207 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
208 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
209 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
210 try_render(conn, "show.json",
213 with_direct_conversation_id: true,
217 _e -> {:error, :not_found}
221 @doc "POST /api/v1/statuses/:id/reblog"
222 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
223 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
224 %Activity{} = announce <- Activity.normalize(announce.data) do
225 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
229 @doc "POST /api/v1/statuses/:id/unreblog"
230 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
231 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
232 %Activity{} = activity <- Activity.get_by_id(activity_id) do
233 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
237 @doc "POST /api/v1/statuses/:id/favourite"
238 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
239 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
240 %Activity{} = activity <- Activity.get_by_id(activity_id) do
241 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
245 @doc "POST /api/v1/statuses/:id/unfavourite"
246 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
247 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
248 %Activity{} = activity <- Activity.get_by_id(activity_id) do
249 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
253 @doc "POST /api/v1/statuses/:id/pin"
254 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
255 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
256 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
260 @doc "POST /api/v1/statuses/:id/unpin"
261 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
262 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
263 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
267 @doc "POST /api/v1/statuses/:id/bookmark"
268 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
269 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
270 %User{} = user <- User.get_cached_by_nickname(user.nickname),
271 true <- Visibility.visible_for_user?(activity, user),
272 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
273 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
277 @doc "POST /api/v1/statuses/:id/unbookmark"
278 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
279 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
280 %User{} = user <- User.get_cached_by_nickname(user.nickname),
281 true <- Visibility.visible_for_user?(activity, user),
282 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
283 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
287 @doc "POST /api/v1/statuses/:id/mute"
288 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
289 with %Activity{} = activity <- Activity.get_by_id(id),
290 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
291 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
295 @doc "POST /api/v1/statuses/:id/unmute"
296 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
297 with %Activity{} = activity <- Activity.get_by_id(id),
298 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
299 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
303 @doc "GET /api/v1/statuses/:id/card"
304 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
305 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
306 with %Activity{} = activity <- Activity.get_by_id(status_id),
307 true <- Visibility.visible_for_user?(activity, user) do
308 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
309 render(conn, "card.json", data)
311 _ -> render_error(conn, :not_found, "Record not found")
315 @doc "GET /api/v1/statuses/:id/favourited_by"
316 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
317 with true <- Pleroma.Config.get([:instance, :show_reactions]),
318 %Activity{} = activity <- Activity.get_by_id_with_object(id),
319 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
320 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
323 |> Ecto.Query.where([u], u.ap_id in ^likes)
325 |> Enum.filter(&(not User.blocks?(user, &1)))
328 |> put_view(AccountView)
329 |> render("index.json", for: user, users: users, as: :user)
331 {:visible, false} -> {:error, :not_found}
336 @doc "GET /api/v1/statuses/:id/reblogged_by"
337 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
338 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
339 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
340 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
341 Object.normalize(activity) do
344 |> Activity.Queries.by_type()
345 |> Ecto.Query.where([a], a.actor in ^announces)
346 # this is to use the index
347 |> Activity.Queries.by_object_id(ap_id)
349 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
350 |> Enum.map(& &1.actor)
355 |> Ecto.Query.where([u], u.ap_id in ^announces)
357 |> Enum.filter(&(not User.blocks?(user, &1)))
360 |> put_view(AccountView)
361 |> render("index.json", for: user, users: users, as: :user)
363 {:visible, false} -> {:error, :not_found}
368 @doc "GET /api/v1/statuses/:id/context"
369 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
370 with %Activity{} = activity <- Activity.get_by_id(id) do
372 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
375 exclude_id: activity.id
378 render(conn, "context.json", activity: activity, activities: activities, user: user)
382 @doc "GET /api/v1/favourites"
383 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
384 activities = ActivityPub.fetch_favourites(user, params)
387 |> add_link_headers(activities)
388 |> render("index.json",
389 activities: activities,
395 @doc "GET /api/v1/bookmarks"
396 def bookmarks(%{assigns: %{user: user}} = conn, params) do
397 user = User.get_cached_by_id(user.id)
401 |> Bookmark.for_user_query()
402 |> Pleroma.Pagination.fetch_paginated(params)
406 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
409 |> add_link_headers(bookmarks)
410 |> render("index.json",
411 activities: activities,