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.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,
125 with_muted: Map.get(params, :with_muted, false)
130 POST /api/v1/statuses
132 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
135 assigns: %{user: user},
136 body_params: %{status: _, scheduled_at: scheduled_at} = params
140 when not is_nil(scheduled_at) do
142 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
143 |> put_application(conn)
146 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
147 scheduled_at: scheduled_at
150 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
151 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
153 |> put_view(ScheduledActivityView)
154 |> render("show.json", scheduled_activity: scheduled_activity)
157 params = Map.drop(params, [:scheduled_at])
158 create(%Plug.Conn{conn | body_params: params}, %{})
165 # Creates a regular status
167 %{assigns: %{user: user}, body_params: %{status: _} = params} = conn,
171 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
172 |> put_application(conn)
174 with {:ok, activity} <- CommonAPI.post(user, params) do
175 try_render(conn, "show.json",
179 with_direct_conversation_id: true
182 {:error, {:reject, message}} ->
184 |> put_status(:unprocessable_entity)
185 |> json(%{error: message})
189 |> put_status(:unprocessable_entity)
190 |> json(%{error: message})
194 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
195 params = Map.put(params, :status, "")
196 create(%Plug.Conn{conn | body_params: params}, %{})
199 @doc "GET /api/v1/statuses/:id"
200 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
201 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
202 true <- Visibility.visible_for_user?(activity, user) do
203 try_render(conn, "show.json",
206 with_direct_conversation_id: true,
207 with_muted: Map.get(params, :with_muted, false)
210 _ -> {:error, :not_found}
214 @doc "DELETE /api/v1/statuses/:id"
215 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
216 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
217 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
218 try_render(conn, "show.json",
221 with_direct_conversation_id: true,
225 _e -> {:error, :not_found}
229 @doc "POST /api/v1/statuses/:id/reblog"
230 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
231 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
232 %Activity{} = announce <- Activity.normalize(announce.data) do
233 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
237 @doc "POST /api/v1/statuses/:id/unreblog"
238 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
239 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
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/favourite"
246 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
247 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
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/unfavourite"
254 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
255 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
256 %Activity{} = activity <- Activity.get_by_id(activity_id) do
257 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
261 @doc "POST /api/v1/statuses/:id/pin"
262 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
263 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
264 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
268 @doc "POST /api/v1/statuses/:id/unpin"
269 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
270 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
271 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
275 @doc "POST /api/v1/statuses/:id/bookmark"
276 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
277 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
278 %User{} = user <- User.get_cached_by_nickname(user.nickname),
279 true <- Visibility.visible_for_user?(activity, user),
280 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
281 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
285 @doc "POST /api/v1/statuses/:id/unbookmark"
286 def unbookmark(%{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.destroy(user.id, activity.id) do
291 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
295 @doc "POST /api/v1/statuses/:id/mute"
296 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
297 with %Activity{} = activity <- Activity.get_by_id(id),
298 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
299 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
303 @doc "POST /api/v1/statuses/:id/unmute"
304 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
305 with %Activity{} = activity <- Activity.get_by_id(id),
306 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
307 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
311 @doc "GET /api/v1/statuses/:id/card"
312 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
313 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
314 with %Activity{} = activity <- Activity.get_by_id(status_id),
315 true <- Visibility.visible_for_user?(activity, user) do
316 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
317 render(conn, "card.json", data)
319 _ -> render_error(conn, :not_found, "Record not found")
323 @doc "GET /api/v1/statuses/:id/favourited_by"
324 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
325 with true <- Pleroma.Config.get([:instance, :show_reactions]),
326 %Activity{} = activity <- Activity.get_by_id_with_object(id),
327 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
328 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
331 |> Ecto.Query.where([u], u.ap_id in ^likes)
333 |> Enum.filter(&(not User.blocks?(user, &1)))
336 |> put_view(AccountView)
337 |> render("index.json", for: user, users: users, as: :user)
339 {:visible, false} -> {:error, :not_found}
344 @doc "GET /api/v1/statuses/:id/reblogged_by"
345 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
346 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
347 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
348 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
349 Object.normalize(activity, fetch: false) do
352 |> Activity.Queries.by_type()
353 |> Ecto.Query.where([a], a.actor in ^announces)
354 # this is to use the index
355 |> Activity.Queries.by_object_id(ap_id)
357 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
358 |> Enum.map(& &1.actor)
363 |> Ecto.Query.where([u], u.ap_id in ^announces)
365 |> Enum.filter(&(not User.blocks?(user, &1)))
368 |> put_view(AccountView)
369 |> render("index.json", for: user, users: users, as: :user)
371 {:visible, false} -> {:error, :not_found}
376 @doc "GET /api/v1/statuses/:id/context"
377 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
378 with %Activity{} = activity <- Activity.get_by_id(id) do
380 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
383 exclude_id: activity.id
386 render(conn, "context.json", activity: activity, activities: activities, user: user)
390 @doc "GET /api/v1/favourites"
391 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
392 activities = ActivityPub.fetch_favourites(user, params)
395 |> add_link_headers(activities)
396 |> render("index.json",
397 activities: activities,
403 @doc "GET /api/v1/bookmarks"
404 def bookmarks(%{assigns: %{user: user}} = conn, params) do
405 user = User.get_cached_by_id(user.id)
409 |> Bookmark.for_user_query()
410 |> Pleroma.Pagination.fetch_paginated(params)
414 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
417 |> add_link_headers(bookmarks)
418 |> render("index.json",
419 activities: activities,
425 defp put_application(params, %{assigns: %{token: %{app_id: app_id}}} = _conn) do
426 params |> Map.put(:application, Pleroma.Web.OAuth.App.get_app_by_id(app_id))
429 defp put_application(params, _), do: Map.put(params, :application, %{name: "Web", website: nil})