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)
32 Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show]
35 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
39 %{@unauthenticated_access | scopes: ["read:statuses"]}
50 %{scopes: ["write:statuses"]}
59 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
63 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
68 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
73 %{@unauthenticated_access | scopes: ["read:accounts"]}
74 when action in [:favourited_by, :reblogged_by]
77 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
79 # Note: scope not present in Mastodon: read:bookmarks
80 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
82 # Note: scope not present in Mastodon: write:bookmarks
85 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
88 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
92 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
93 when action in ~w(reblog unreblog)a
98 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
99 when action in ~w(favourite unfavourite)a
102 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
104 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
106 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
109 GET `/api/v1/statuses?ids[]=1&ids[]=2`
111 `ids` query param is required
113 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
119 |> Activity.all_by_ids_with_object()
120 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
122 render(conn, "index.json",
123 activities: activities,
126 with_muted: Map.get(params, :with_muted, false)
131 POST /api/v1/statuses
133 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
136 assigns: %{user: user},
137 body_params: %{status: _, scheduled_at: scheduled_at} = params
141 when not is_nil(scheduled_at) do
143 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
144 |> put_application(conn)
147 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
148 scheduled_at: scheduled_at
151 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
152 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
154 |> put_view(ScheduledActivityView)
155 |> render("show.json", scheduled_activity: scheduled_activity)
158 params = Map.drop(params, [:scheduled_at])
159 create(%Plug.Conn{conn | body_params: params}, %{})
166 # Creates a regular status
167 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
169 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
170 |> put_application(conn)
172 with {:ok, activity} <- CommonAPI.post(user, params) do
173 try_render(conn, "show.json",
177 with_direct_conversation_id: true
180 {:error, {:reject, message}} ->
182 |> put_status(:unprocessable_entity)
183 |> json(%{error: message})
187 |> put_status(:unprocessable_entity)
188 |> json(%{error: message})
192 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
193 params = Map.put(params, :status, "")
194 create(%Plug.Conn{conn | body_params: params}, %{})
197 @doc "GET /api/v1/statuses/:id"
198 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
199 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
200 true <- Visibility.visible_for_user?(activity, user) do
201 try_render(conn, "show.json",
204 with_direct_conversation_id: true,
205 with_muted: Map.get(params, :with_muted, false)
208 _ -> {:error, :not_found}
212 @doc "DELETE /api/v1/statuses/:id"
213 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
214 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
215 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
216 try_render(conn, "show.json",
219 with_direct_conversation_id: true,
223 _e -> {:error, :not_found}
227 @doc "POST /api/v1/statuses/:id/reblog"
228 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
229 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
230 %Activity{} = announce <- Activity.normalize(announce.data) do
231 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
235 @doc "POST /api/v1/statuses/:id/unreblog"
236 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
237 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
238 %Activity{} = activity <- Activity.get_by_id(activity_id) do
239 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
243 @doc "POST /api/v1/statuses/:id/favourite"
244 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
245 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
246 %Activity{} = activity <- Activity.get_by_id(activity_id) do
247 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
251 @doc "POST /api/v1/statuses/:id/unfavourite"
252 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
253 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
254 %Activity{} = activity <- Activity.get_by_id(activity_id) do
255 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
259 @doc "POST /api/v1/statuses/:id/pin"
260 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
261 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
262 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
264 {:error, :pinned_statuses_limit_reached} ->
265 {:error, "You have already pinned the maximum number of statuses"}
267 {:error, :ownership_error} ->
268 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
270 {:error, :visibility_error} ->
271 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
278 @doc "POST /api/v1/statuses/:id/unpin"
279 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
280 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
281 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
285 @doc "POST /api/v1/statuses/:id/bookmark"
286 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
287 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
288 %User{} = user <- User.get_cached_by_nickname(user.nickname),
289 true <- Visibility.visible_for_user?(activity, user),
290 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
291 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
295 @doc "POST /api/v1/statuses/:id/unbookmark"
296 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
297 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
298 %User{} = user <- User.get_cached_by_nickname(user.nickname),
299 true <- Visibility.visible_for_user?(activity, user),
300 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
301 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
305 @doc "POST /api/v1/statuses/:id/mute"
306 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
307 with %Activity{} = activity <- Activity.get_by_id(id),
308 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
309 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
313 @doc "POST /api/v1/statuses/:id/unmute"
314 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
315 with %Activity{} = activity <- Activity.get_by_id(id),
316 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
317 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
321 @doc "GET /api/v1/statuses/:id/card"
322 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
323 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
324 with %Activity{} = activity <- Activity.get_by_id(status_id),
325 true <- Visibility.visible_for_user?(activity, user) do
326 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
327 render(conn, "card.json", data)
329 _ -> render_error(conn, :not_found, "Record not found")
333 @doc "GET /api/v1/statuses/:id/favourited_by"
334 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
335 with true <- Pleroma.Config.get([:instance, :show_reactions]),
336 %Activity{} = activity <- Activity.get_by_id_with_object(id),
337 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
338 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
341 |> Ecto.Query.where([u], u.ap_id in ^likes)
343 |> Enum.filter(&(not User.blocks?(user, &1)))
346 |> put_view(AccountView)
347 |> render("index.json", for: user, users: users, as: :user)
349 {:visible, false} -> {:error, :not_found}
354 @doc "GET /api/v1/statuses/:id/reblogged_by"
355 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
356 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
357 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
358 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
359 Object.normalize(activity, fetch: false) do
362 |> Activity.Queries.by_type()
363 |> Ecto.Query.where([a], a.actor in ^announces)
364 # this is to use the index
365 |> Activity.Queries.by_object_id(ap_id)
367 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
368 |> Enum.map(& &1.actor)
373 |> Ecto.Query.where([u], u.ap_id in ^announces)
375 |> Enum.filter(&(not User.blocks?(user, &1)))
378 |> put_view(AccountView)
379 |> render("index.json", for: user, users: users, as: :user)
381 {:visible, false} -> {:error, :not_found}
386 @doc "GET /api/v1/statuses/:id/context"
387 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
388 with %Activity{} = activity <- Activity.get_by_id(id) do
390 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
393 exclude_id: activity.id
396 render(conn, "context.json", activity: activity, activities: activities, user: user)
400 @doc "GET /api/v1/favourites"
401 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
402 activities = ActivityPub.fetch_favourites(user, params)
405 |> add_link_headers(activities)
406 |> render("index.json",
407 activities: activities,
413 @doc "GET /api/v1/bookmarks"
414 def bookmarks(%{assigns: %{user: user}} = conn, params) do
415 user = User.get_cached_by_id(user.id)
419 |> Bookmark.for_user_query()
420 |> Pleroma.Pagination.fetch_paginated(params)
424 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
427 |> add_link_headers(bookmarks)
428 |> render("index.json",
429 activities: activities,
435 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
436 if user.disclose_client do
437 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
438 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
440 Map.put(params, :generator, nil)
444 defp put_application(params, _), do: Map.put(params, :generator, nil)