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
177 |> put_status(:unprocessable_entity)
178 |> json(%{error: message})
182 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
183 params = Map.put(params, :status, "")
184 create(%Plug.Conn{conn | body_params: params}, %{})
187 @doc "GET /api/v1/statuses/:id"
188 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
189 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
190 true <- Visibility.visible_for_user?(activity, user) do
191 try_render(conn, "show.json",
194 with_direct_conversation_id: true
197 _ -> {:error, :not_found}
201 @doc "DELETE /api/v1/statuses/:id"
202 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
203 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
205 try_render(conn, "show.json",
208 with_direct_conversation_id: true,
211 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
214 _e -> {:error, :not_found}
218 @doc "POST /api/v1/statuses/:id/reblog"
219 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
220 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
221 %Activity{} = announce <- Activity.normalize(announce.data) do
222 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
226 @doc "POST /api/v1/statuses/:id/unreblog"
227 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
228 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
229 %Activity{} = activity <- Activity.get_by_id(activity_id) do
230 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
234 @doc "POST /api/v1/statuses/:id/favourite"
235 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
236 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
237 %Activity{} = activity <- Activity.get_by_id(activity_id) do
238 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
242 @doc "POST /api/v1/statuses/:id/unfavourite"
243 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
244 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
245 %Activity{} = activity <- Activity.get_by_id(activity_id) do
246 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
250 @doc "POST /api/v1/statuses/:id/pin"
251 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
252 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
253 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
257 @doc "POST /api/v1/statuses/:id/unpin"
258 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
259 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
260 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
264 @doc "POST /api/v1/statuses/:id/bookmark"
265 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
266 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
267 %User{} = user <- User.get_cached_by_nickname(user.nickname),
268 true <- Visibility.visible_for_user?(activity, user),
269 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
270 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
274 @doc "POST /api/v1/statuses/:id/unbookmark"
275 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
276 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
277 %User{} = user <- User.get_cached_by_nickname(user.nickname),
278 true <- Visibility.visible_for_user?(activity, user),
279 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
280 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
284 @doc "POST /api/v1/statuses/:id/mute"
285 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
286 with %Activity{} = activity <- Activity.get_by_id(id),
287 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
288 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
292 @doc "POST /api/v1/statuses/:id/unmute"
293 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
294 with %Activity{} = activity <- Activity.get_by_id(id),
295 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
296 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
300 @doc "GET /api/v1/statuses/:id/card"
301 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
302 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
303 with %Activity{} = activity <- Activity.get_by_id(status_id),
304 true <- Visibility.visible_for_user?(activity, user) do
305 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
306 render(conn, "card.json", data)
308 _ -> render_error(conn, :not_found, "Record not found")
312 @doc "GET /api/v1/statuses/:id/favourited_by"
313 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
314 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
315 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
316 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
319 |> Ecto.Query.where([u], u.ap_id in ^likes)
321 |> Enum.filter(&(not User.blocks?(user, &1)))
324 |> put_view(AccountView)
325 |> render("index.json", for: user, users: users, as: :user)
327 {:visible, false} -> {:error, :not_found}
332 @doc "GET /api/v1/statuses/:id/reblogged_by"
333 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
334 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
335 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
336 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
337 Object.normalize(activity) do
340 |> Activity.Queries.by_type()
341 |> Ecto.Query.where([a], a.actor in ^announces)
342 # this is to use the index
343 |> Activity.Queries.by_object_id(ap_id)
345 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
346 |> Enum.map(& &1.actor)
351 |> Ecto.Query.where([u], u.ap_id in ^announces)
353 |> Enum.filter(&(not User.blocks?(user, &1)))
356 |> put_view(AccountView)
357 |> render("index.json", for: user, users: users, as: :user)
359 {:visible, false} -> {:error, :not_found}
364 @doc "GET /api/v1/statuses/:id/context"
365 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
366 with %Activity{} = activity <- Activity.get_by_id(id) do
368 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
371 exclude_id: activity.id
374 render(conn, "context.json", activity: activity, activities: activities, user: user)
378 @doc "GET /api/v1/favourites"
379 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
380 activities = ActivityPub.fetch_favourites(user, params)
383 |> add_link_headers(activities)
384 |> render("index.json",
385 activities: activities,
391 @doc "GET /api/v1/bookmarks"
392 def bookmarks(%{assigns: %{user: user}} = conn, params) do
393 user = User.get_cached_by_id(user.id)
397 |> Bookmark.for_user_query()
398 |> Pleroma.Pagination.fetch_paginated(params)
402 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
405 |> add_link_headers(bookmarks)
406 |> render("index.json",
407 activities: activities,