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)
266 @doc "POST /api/v1/statuses/:id/unpin"
267 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
268 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
269 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
273 @doc "POST /api/v1/statuses/:id/bookmark"
274 def bookmark(%{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.create(user.id, activity.id) do
279 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
283 @doc "POST /api/v1/statuses/:id/unbookmark"
284 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
285 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
286 %User{} = user <- User.get_cached_by_nickname(user.nickname),
287 true <- Visibility.visible_for_user?(activity, user),
288 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
289 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
293 @doc "POST /api/v1/statuses/:id/mute"
294 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
295 with %Activity{} = activity <- Activity.get_by_id(id),
296 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
297 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
301 @doc "POST /api/v1/statuses/:id/unmute"
302 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
303 with %Activity{} = activity <- Activity.get_by_id(id),
304 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
305 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
309 @doc "GET /api/v1/statuses/:id/card"
310 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
311 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
312 with %Activity{} = activity <- Activity.get_by_id(status_id),
313 true <- Visibility.visible_for_user?(activity, user) do
314 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
315 render(conn, "card.json", data)
317 _ -> render_error(conn, :not_found, "Record not found")
321 @doc "GET /api/v1/statuses/:id/favourited_by"
322 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
323 with true <- Pleroma.Config.get([:instance, :show_reactions]),
324 %Activity{} = activity <- Activity.get_by_id_with_object(id),
325 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
326 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
329 |> Ecto.Query.where([u], u.ap_id in ^likes)
331 |> Enum.filter(&(not User.blocks?(user, &1)))
334 |> put_view(AccountView)
335 |> render("index.json", for: user, users: users, as: :user)
337 {:visible, false} -> {:error, :not_found}
342 @doc "GET /api/v1/statuses/:id/reblogged_by"
343 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
344 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
345 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
346 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
347 Object.normalize(activity, fetch: false) do
350 |> Activity.Queries.by_type()
351 |> Ecto.Query.where([a], a.actor in ^announces)
352 # this is to use the index
353 |> Activity.Queries.by_object_id(ap_id)
355 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
356 |> Enum.map(& &1.actor)
361 |> Ecto.Query.where([u], u.ap_id in ^announces)
363 |> Enum.filter(&(not User.blocks?(user, &1)))
366 |> put_view(AccountView)
367 |> render("index.json", for: user, users: users, as: :user)
369 {:visible, false} -> {:error, :not_found}
374 @doc "GET /api/v1/statuses/:id/context"
375 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
376 with %Activity{} = activity <- Activity.get_by_id(id) do
378 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
381 exclude_id: activity.id
384 render(conn, "context.json", activity: activity, activities: activities, user: user)
388 @doc "GET /api/v1/favourites"
389 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
390 activities = ActivityPub.fetch_favourites(user, params)
393 |> add_link_headers(activities)
394 |> render("index.json",
395 activities: activities,
401 @doc "GET /api/v1/bookmarks"
402 def bookmarks(%{assigns: %{user: user}} = conn, params) do
403 user = User.get_cached_by_id(user.id)
407 |> Bookmark.for_user_query()
408 |> Pleroma.Pagination.fetch_paginated(params)
412 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
415 |> add_link_headers(bookmarks)
416 |> render("index.json",
417 activities: activities,
423 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
424 if user.disclose_client do
425 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
426 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
428 Map.put(params, :generator, nil)
432 defp put_application(params, _), do: Map.put(params, :generator, nil)