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.OAuth.Token
25 alias Pleroma.Web.Plugs.OAuthScopesPlug
26 alias Pleroma.Web.Plugs.RateLimiter
28 plug(Pleroma.Web.ApiSpec.CastAndValidate)
30 plug(:skip_public_check when action in [:index, :show])
32 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
36 %{@unauthenticated_access | scopes: ["read:statuses"]}
47 %{scopes: ["write:statuses"]}
56 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
60 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
65 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
70 %{@unauthenticated_access | scopes: ["read:accounts"]}
71 when action in [:favourited_by, :reblogged_by]
74 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
76 # Note: scope not present in Mastodon: read:bookmarks
77 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
79 # Note: scope not present in Mastodon: write:bookmarks
82 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
85 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
89 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
90 when action in ~w(reblog unreblog)a
95 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
96 when action in ~w(favourite unfavourite)a
99 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
101 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
103 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
106 GET `/api/v1/statuses?ids[]=1&ids[]=2`
108 `ids` query param is required
110 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
116 |> Activity.all_by_ids_with_object()
117 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
119 render(conn, "index.json",
120 activities: activities,
123 with_muted: Map.get(params, :with_muted, false)
128 POST /api/v1/statuses
130 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
133 assigns: %{user: user},
134 body_params: %{status: _, scheduled_at: scheduled_at} = params
138 when not is_nil(scheduled_at) do
140 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
141 |> put_application(conn)
144 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
145 scheduled_at: scheduled_at
148 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
149 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
151 |> put_view(ScheduledActivityView)
152 |> render("show.json", scheduled_activity: scheduled_activity)
155 params = Map.drop(params, [:scheduled_at])
156 create(%Plug.Conn{conn | body_params: params}, %{})
163 # Creates a regular status
164 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
166 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
167 |> put_application(conn)
169 with {:ok, activity} <- CommonAPI.post(user, params) do
170 try_render(conn, "show.json",
174 with_direct_conversation_id: true
177 {:error, {:reject, message}} ->
179 |> put_status(:unprocessable_entity)
180 |> json(%{error: message})
184 |> put_status(:unprocessable_entity)
185 |> json(%{error: message})
189 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
190 params = Map.put(params, :status, "")
191 create(%Plug.Conn{conn | body_params: params}, %{})
194 @doc "GET /api/v1/statuses/:id"
195 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
196 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
197 true <- Visibility.visible_for_user?(activity, user) do
198 try_render(conn, "show.json",
201 with_direct_conversation_id: true,
202 with_muted: Map.get(params, :with_muted, false)
205 _ -> {:error, :not_found}
209 @doc "DELETE /api/v1/statuses/:id"
210 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
211 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
212 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
213 try_render(conn, "show.json",
216 with_direct_conversation_id: true,
220 _e -> {:error, :not_found}
224 @doc "POST /api/v1/statuses/:id/reblog"
225 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
226 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
227 %Activity{} = announce <- Activity.normalize(announce.data) do
228 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
232 @doc "POST /api/v1/statuses/:id/unreblog"
233 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
234 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
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/favourite"
241 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
242 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
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/unfavourite"
249 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
250 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
251 %Activity{} = activity <- Activity.get_by_id(activity_id) do
252 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
256 @doc "POST /api/v1/statuses/:id/pin"
257 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
258 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
259 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
261 {:error, :pinned_statuses_limit_reached} ->
262 {:error, "You have already pinned the maximum number of statuses"}
264 {:error, :ownership_error} ->
265 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
267 {:error, :visibility_error} ->
268 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
275 @doc "POST /api/v1/statuses/:id/unpin"
276 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
277 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
278 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
282 @doc "POST /api/v1/statuses/:id/bookmark"
283 def bookmark(%{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.create(user.id, activity.id) do
288 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
292 @doc "POST /api/v1/statuses/:id/unbookmark"
293 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
294 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
295 %User{} = user <- User.get_cached_by_nickname(user.nickname),
296 true <- Visibility.visible_for_user?(activity, user),
297 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
298 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
302 @doc "POST /api/v1/statuses/:id/mute"
303 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
304 with %Activity{} = activity <- Activity.get_by_id(id),
305 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
306 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
310 @doc "POST /api/v1/statuses/:id/unmute"
311 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
312 with %Activity{} = activity <- Activity.get_by_id(id),
313 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
314 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
318 @doc "GET /api/v1/statuses/:id/card"
319 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
320 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
321 with %Activity{} = activity <- Activity.get_by_id(status_id),
322 true <- Visibility.visible_for_user?(activity, user) do
323 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
324 render(conn, "card.json", data)
326 _ -> render_error(conn, :not_found, "Record not found")
330 @doc "GET /api/v1/statuses/:id/favourited_by"
331 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
332 with true <- Pleroma.Config.get([:instance, :show_reactions]),
333 %Activity{} = activity <- Activity.get_by_id_with_object(id),
334 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
335 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
338 |> Ecto.Query.where([u], u.ap_id in ^likes)
340 |> Enum.filter(&(not User.blocks?(user, &1)))
343 |> put_view(AccountView)
344 |> render("index.json", for: user, users: users, as: :user)
346 {:visible, false} -> {:error, :not_found}
351 @doc "GET /api/v1/statuses/:id/reblogged_by"
352 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
353 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
354 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
355 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
356 Object.normalize(activity, fetch: false) do
359 |> Activity.Queries.by_type()
360 |> Ecto.Query.where([a], a.actor in ^announces)
361 # this is to use the index
362 |> Activity.Queries.by_object_id(ap_id)
364 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
365 |> Enum.map(& &1.actor)
370 |> Ecto.Query.where([u], u.ap_id in ^announces)
372 |> Enum.filter(&(not User.blocks?(user, &1)))
375 |> put_view(AccountView)
376 |> render("index.json", for: user, users: users, as: :user)
378 {:visible, false} -> {:error, :not_found}
383 @doc "GET /api/v1/statuses/:id/context"
384 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
385 with %Activity{} = activity <- Activity.get_by_id(id) do
387 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
390 exclude_id: activity.id
393 render(conn, "context.json", activity: activity, activities: activities, user: user)
397 @doc "GET /api/v1/favourites"
398 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
399 activities = ActivityPub.fetch_favourites(user, params)
402 |> add_link_headers(activities)
403 |> render("index.json",
404 activities: activities,
410 @doc "GET /api/v1/bookmarks"
411 def bookmarks(%{assigns: %{user: user}} = conn, params) do
412 user = User.get_cached_by_id(user.id)
416 |> Bookmark.for_user_query()
417 |> Pleroma.Pagination.fetch_paginated(params)
421 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
424 |> add_link_headers(bookmarks)
425 |> render("index.json",
426 activities: activities,
432 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
433 if user.disclose_client do
434 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
435 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
437 Map.put(params, :generator, nil)
441 defp put_application(params, _), do: Map.put(params, :generator, nil)